* feat(#2975): adopt changeset-fragment workflow to eliminate CHANGELOG conflicts
Two PRs that both edit `### Fixed` in CHANGELOG.md always conflict on merge.
Recently bit on #2960/#2972 in the same session — fix-the-conflict-and-rebase
tax. Replace the shared-file model with per-PR fragment files that never
share lines.
Implementation built TDD per #2975, vertical slices with structured-IR
assertions throughout:
scripts/changeset/parse.cjs - fragment text → typed record + frozen
FRAGMENT_ERROR enum (8 tests)
scripts/changeset/render.cjs - fragments → structured IR with
Keep-a-Changelog section ordering
(2 tests)
scripts/changeset/serialize.cjs - IR ↔ markdown round-trip pair
(parse(serialize(ir)) === ir,
3 tests)
scripts/changeset/cli.cjs - file-I/O wrapper with --json mode;
reads .changeset/, folds into
CHANGELOG.md, deletes consumed
fragments. Idempotent. (1 test)
scripts/changeset/lint.cjs - pure verdict (changedFiles, labels)
→ { ok, reason } via LINT_REASON
enum. Honors `no-changelog` label.
(5 tests)
scripts/changeset/new.cjs - fragment scaffolder with random
adjective-noun-noun filename. Tests
assert via parseFragment round-trip.
(3 tests)
Total: 22 tests, all assertions on typed structured fields. No regex on
text, no String#includes on file content. Lint clean across 356 test files.
Supporting:
.changeset/README.md - format spec + workflow docs
.changeset/eager-hawks-rally.md - dogfood fragment for THIS PR (will
be the first thing the new release
tool consumes)
.github/workflows/changeset-required.yml
- CI: every PR runs lint.cjs
package.json - npm run changeset, changelog:render,
lint:changeset
CONTRIBUTING.md - new "CHANGELOG Entries — Drop a
Fragment" section between PR
Guidelines and Testing Standards
Closes#2975
* fix(#2975): address CodeRabbit findings on changeset workflow
7 valid findings (4 Major, 3 Minor); all addressed:
scripts/changeset/parse.cjs
- Preserve fragment body verbatim. Previously body.trim() ate
intentional leading whitespace (code blocks, etc.); now trim() is
used only for the emptiness check, and a single trailing newline
is stripped (the editor-added one) so well-formed fragments
round-trip byte-for-byte. Added a regression test asserting a
code-block-leading body is preserved.
scripts/changeset/cli.cjs
- Validate flag values during argument parsing. parseArgs now returns
{ ok, opts | error }; rejects `--repo` etc. with no following value
or with another flag as the value. main() surfaces the error
message before exiting 2.
- Handle post-write fragment-deletion failures. After CHANGELOG.md
is written, any unlink failure is captured into a structured
deleteFailures list with reason 'fail_fragment_delete'; cmdRender
returns exitCode=1 with the partial-failure detail instead of
leaving the changelog updated and fragments behind (which would
cause double-consumption on rerun).
scripts/changeset/lint.cjs
- Treat CHANGELOG.md as a linted user-facing path. Direct edits to
CHANGELOG.md (the bypass route around the new workflow) now fail
the lint with FAIL_MISSING_FRAGMENT. Added a regression test for
that case.
- Use cp.execFileSync instead of cp.execSync for the git diff call.
Eliminates the shell-interpolation surface on GITHUB_BASE_REF;
git's own arg parser remains the validator.
scripts/changeset/new.cjs
- Atomic fragment creation. existsSync() + writeFileSync was racy
under concurrent invocations. Now writeFileSync uses { flag: 'wx' }
which fails EEXIST on collision; the random-name retry loop
catches EEXIST and re-rolls. Throws explicitly after 16 attempts
rather than silently overwriting.
.changeset/README.md
- Add language tag `md` to the format example fence (markdownlint
MD040).
All 25 changeset tests pass; lint clean (356 test files, 0 violations).
* fix(#2975): sanitize --type and validate flag values in new.cjs (CR fixes)
Two CR findings on scripts/changeset/new.cjs:
1. (Minor) `type` was embedded in frontmatter without sanitization. A
newline in the value (e.g. `--type 'Fixed\ntype: Added'`) would
corrupt the fragment. scaffoldFragment now validates `type` against
the Keep-a-Changelog ALLOWED_TYPES set BEFORE writing — same set
parse.cjs uses on consume. Throws with a typed error referencing
the allowed values; tests cover the newline case + 4 other
non-allowed values.
2. (Minor) `--repo` (and other value-taking flags) without a value
silently set opts.repo to undefined, which produced a cryptic
ERR_INVALID_ARG_TYPE deep inside path.join. parseArgs now mirrors
the cli.cjs convention: returns { ok, opts | error }, validates
that the next token exists and is not itself another flag, and
surfaces a precise "missing value for --repo" message before exit.
Added 3 tests: missing-trailing-value, flag-as-value, well-formed.
29 tests pass across the changeset suite (4 new regression tests).
* fix(#2791): GSD_WORKSTREAM env var respected by gsd-sdk query + gsd-tools bin alias
Two fixes for gsd-sdk binary issues:
**Issue 1 — Binary name collision:**
Both `get-shit-done-cc` and `@gsd-build/sdk` declare `bin: { "gsd-sdk": ... }`.
Added `"gsd-tools": "bin/gsd-sdk.js"` to `package.json` bin so users with the
collision can invoke `gsd-tools query <cmd>` as a conflict-free alternative.
**Issue 2 — Query registry not workstream-aware:**
`gsd-sdk query` commands ignored `GSD_WORKSTREAM` env var, always reading from
the root `.planning/` even when a workstream was active. `gsd-tools.cjs` reads
`GSD_WORKSTREAM` via `planningDir()`, so all ~35 `gsd-sdk query` call sites in
workflow files were broken in workstream-scoped projects.
Fix: added env var fallback in `sdk/src/cli.ts` — when `--ws` is not provided,
`GSD_WORKSTREAM` is used (with name validation; invalid values are silently
ignored, matching CJS behaviour).
Regression test: `tests/bug-2791-sdk-workstream-env.test.cjs`
Closes#2791
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#2791): address CodeRabbit — precedence test, invalid env fallback assertion, bash fence
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(test-standards): enforce no-source-grep rule with CI linter + update CONTRIBUTING.md
Adds scripts/lint-no-source-grep.cjs — a static linter that detects readFileSync
on .cjs source files in tests without an allow-test-rule annotation. Wires it
into CI as a new lint-tests job in test.yml and as npm run lint:tests.
Resolves all 9 existing violations across the test suite:
- Rewrites workspace routing tests (3) as behavioral runGsdTools calls that
verify each command is router-recognized (exit != "Unknown init workflow")
- Adds allow-test-rule annotations with explanatory comments to 7 legitimate
structural tests: architectural invariants (locking, orphan-worktree),
structural regression guards (milestone-regex-global), docs-parity
(config-field-docs), integration-test-input (copilot-install), and
structural-implementation-guards (bug-1891, discuss-mode)
Updates CONTRIBUTING.md Testing Standards section with:
- "Prohibited: Source-Grep Tests" section with the before/after pattern,
root cause analysis of why it breaks (commit 990c3e64), and CI reference
- allow-test-rule exemption table (6 recognized categories with when-to-use)
- "CI Test Quality Checks" table showing lint-tests job and local run command
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: resolve CodeRabbit findings on PR #2700
- CONTRIBUTING.md: "four recognized categories" → "six" (table has 6 rows)
- workspace.test.cjs: use positional args in routing tests (no --name flag)
- lint-no-source-grep.cjs: add source-dir guard to READ_WITH_INLINE_CJS_RE
(mirrors CJS_PATH_CONST_RE's protection against false positives on temp files)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(lint): tighten allow-test-rule and add recursive test discovery
- ALLOW_ANNOTATION now requires at least one non-whitespace char after the
colon so bare '// allow-test-rule:' cannot bypass the lint gate
- findTestFiles() recurses into subdirectories so nested *.test.cjs files
are covered if the tests/ tree ever grows subdirs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* 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>
PR #2386 v1 installed the published @gsd-build/sdk from npm, which ships an
older version that lacks query handlers needed by current workflows. Every
GSD release would drift further from what the installer put on PATH.
This commit rewires installSdkIfNeeded() to build from the in-repo sdk/
source tree instead:
1. cd sdk && npm install (build-time deps incl. tsc)
2. npm run build (tsc → sdk/dist/)
3. npm install -g . (global install; gsd-sdk on PATH)
Each step is a hard gate — failures warn loudly and point users at the
manual equivalent command. No more silent drift between installed SDK and
the rest of the GSD system.
Root package.json `files` now ships sdk/src, sdk/prompts, sdk/package.json,
sdk/package-lock.json, and sdk/tsconfig.json so npm-registry installs also
carry the source tree needed to build gsd-sdk locally.
Also fixes a blocking tsc error in sdk/src/event-stream.ts:313 — the cast
to `Array<{ type: string; [key: string]: unknown }>` needed a double-cast
via `unknown` because BetaContentBlock's variants don't carry an index
signature. Runtime-neutral type-widening; sdk vitest suite unchanged
(1256 passing; the lone failure is a pre-existing integration test that
requires external API access).
Updates the #1657/#2385 regression test to assert the new build-from-source
path (path.resolve(__dirname, '..', 'sdk') + `npm run build` + `npm install
-g .`) plus a new assertion that root package.json files array ships sdk
source.
Refs #2385
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Node 22 is still in Active LTS until October 2026 and Maintenance LTS
until April 2027. Raising the engines floor to >=24.0.0 unnecessarily
locked out a fully-supported LTS version and produced EBADENGINE
warnings on install. Restore Node 22 support, add Node 22 to the CI
matrix, and update CONTRIBUTING.md to match.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The "files" field in package.json listed "hooks/dist" instead of "hooks",
which excluded gsd-session-state.sh, gsd-validate-commit.sh, and
gsd-phase-boundary.sh from the npm tarball. Any fresh install from the
registry produced broken shell hook registrations.
Fix: replace "hooks/dist" with "hooks" so the full hooks/ directory is
bundled, covering both the compiled .js files (in hooks/dist/) and the
.sh source hooks at the top of hooks/.
Adds regression test in tests/package-manifest.test.cjs.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Node 20 reached EOL April 30 2026. Node 22 is no longer the LTS
baseline — Node 24 is the current Active LTS. Update CI matrix to
run only Node 24, raise engines floor to >=24.0.0, and update
CONTRIBUTING.md node compatibility table accordingly.
Fixes#1847
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the Windows CI runner with a static analysis test that catches
the same class of platform-specific path bugs (C:\, /home/, /Users/,
/tmp/) without requiring an actual Windows machine.
- tests/hardcoded-paths.test.cjs: new static scanner that checks string
literals in all source JS/CJS files for hardcoded platform paths;
runs on Linux/macOS in <100ms and fires on every PR
- .github/workflows/test.yml: remove windows-latest from matrix; switch
macOS smoke-test runner from Node 22 → Node 24 (the declared standard)
- package.json: bump engines.node from >=20.0.0 to >=22.0.0 (Node 20
reached EOL April 2026)
Matrix goes from 4 runners → 3 runners per run:
ubuntu/22 ubuntu/24 macos/24
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The repository was transferred from the glittercowboy org to gsd-build,
but several files still referenced the old org in URLs. This updates all
repository URL references across READMEs (all languages), package.json,
and the update workflow. Also removes a duplicate language selector in
the main README header.
Files intentionally unchanged:
- CHANGELOG.md (historical entries)
- CODEOWNERS, FUNDING.yml, SECURITY.md (reference @glittercowboy as a
GitHub username/handle, not a repo URL)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Node 18 reached EOL April 2025. Node 24 is the current LTS target.
Changes:
- CI matrix: [18, 20, 22] → [20, 22, 24]
- package.json engines: >=16.7.0 → >=20.0.0
- Removed Node 18 conditional in CI (c8 coverage works on all 20+)
- Simplified CI to single test:coverage step for all versions
797/797 tests pass on Node 24.
npm scripts pass `tests/*.test.cjs` to node/c8 as a literal string on
Windows (PowerShell/cmd don't expand globs). Adding `shell: bash` to CI
steps doesn't help because c8 spawns node as a child process using the
system shell. Use a Node script to enumerate test files cross-platform.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>