Merge remote-tracking branch 'origin/main' into feat/2387-ingest-docs-clean

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
Jeremy McSpadden
2026-04-17 21:46:39 -05:00
7 changed files with 171 additions and 11 deletions

View File

@@ -9,6 +9,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Added
- **`/gsd-ingest-docs` command** — Scan a repo containing mixed ADRs, PRDs, SPECs, and DOCs and bootstrap or merge the full `.planning/` setup from them in a single pass. Parallel classification (`gsd-doc-classifier`), synthesis with precedence rules and cycle detection (`gsd-doc-synthesizer`), three-bucket conflicts report (`INGEST-CONFLICTS.md`: auto-resolved, competing-variants, unresolved-blockers), and hard-block on LOCKED-vs-LOCKED ADR contradictions in both new and merge modes. Supports directory-convention discovery and `--manifest <file>` YAML override with per-doc precedence. v1 caps at 50 docs per invocation; `--resolve interactive` is reserved. Extracts shared conflict-detection contract into `references/doc-conflict-engine.md` which `/gsd-import` now also consumes (#2387)
### Fixed
- **Installer now installs `@gsd-build/sdk` automatically** so `gsd-sdk` lands on PATH. Resolves `command not found: gsd-sdk` errors that affected every `/gsd-*` command after a fresh install or `/gsd-update` to 1.36+. Adds `--no-sdk` to opt out and `--sdk` to force reinstall. Implements the `--sdk` flag that was previously documented in README but never wired up (#2385)
## [1.37.1] - 2026-04-17
### Fixed

View File

@@ -193,7 +193,7 @@ npx get-shit-done-cc --all --global # Install to all directories
Use `--global` (`-g`) or `--local` (`-l`) to skip the location prompt.
Use `--claude`, `--opencode`, `--gemini`, `--kilo`, `--codex`, `--copilot`, `--cursor`, `--windsurf`, `--antigravity`, `--augment`, `--trae`, `--qwen`, `--codebuddy`, `--cline`, or `--all` to skip the runtime prompt.
Use `--sdk` to also install the GSD SDK CLI (`gsd-sdk`) for headless autonomous execution.
The GSD SDK CLI (`gsd-sdk`) is installed automatically (required by `/gsd-*` commands). Pass `--no-sdk` to skip the SDK install, or `--sdk` to force a reinstall.
</details>

View File

@@ -77,6 +77,13 @@ const hasBoth = args.includes('--both'); // Legacy flag, keeps working
const hasAll = args.includes('--all');
const hasUninstall = args.includes('--uninstall') || args.includes('-u');
const hasPortableHooks = args.includes('--portable-hooks') || process.env.GSD_PORTABLE_HOOKS === '1';
const hasSdk = args.includes('--sdk');
const hasNoSdk = args.includes('--no-sdk');
if (hasSdk && hasNoSdk) {
console.error(` ${yellow}Cannot specify both --sdk and --no-sdk${reset}`);
process.exit(1);
}
// Runtime selection - can be set by flags or interactive prompt
let selectedRuntimes = [];
@@ -6626,6 +6633,95 @@ function promptLocation(runtimes) {
});
}
/**
* Build `@gsd-build/sdk` from the in-repo `sdk/` source tree and install the
* resulting `gsd-sdk` binary globally so workflow commands that shell out to
* `gsd-sdk query …` succeed.
*
* We build from source rather than `npm install -g @gsd-build/sdk` because the
* npm-published package lags the source tree and shipping a stale SDK breaks
* every /gsd-* command that depends on newer query handlers.
*
* Skip if --no-sdk. Skip if already on PATH (unless --sdk was explicit).
* Failures are warnings, not fatal.
*/
function installSdkIfNeeded() {
if (hasNoSdk) {
console.log(`\n ${dim}Skipping GSD SDK install (--no-sdk)${reset}`);
return;
}
const { spawnSync } = require('child_process');
const path = require('path');
const fs = require('fs');
if (!hasSdk) {
const probe = spawnSync(process.platform === 'win32' ? 'where' : 'which', ['gsd-sdk'], { stdio: 'ignore' });
if (probe.status === 0) {
console.log(` ${green}✓${reset} GSD SDK already installed (gsd-sdk on PATH)`);
return;
}
}
// Locate the in-repo sdk/ directory relative to this installer file.
// For global npm installs this resolves inside the published package dir;
// for git-based installs (npx github:..., local clone) it resolves to the
// repo's sdk/ tree. Both contain the source tree because root package.json
// includes "sdk" in its `files` array.
const sdkDir = path.resolve(__dirname, '..', 'sdk');
const sdkPackageJson = path.join(sdkDir, 'package.json');
const warnManual = (reason) => {
console.warn(` ${yellow}⚠${reset} ${reason}`);
console.warn(` Build manually from the repo sdk/ directory:`);
console.warn(` ${cyan}cd ${sdkDir} && npm install && npm run build && npm install -g .${reset}`);
console.warn(` Then restart your shell so the updated PATH is picked up.`);
console.warn(` Without it, /gsd-* commands will fail with "command not found: gsd-sdk".`);
};
if (!fs.existsSync(sdkPackageJson)) {
warnManual(`SDK source tree not found at ${sdkDir}.`);
return;
}
console.log(`\n ${cyan}Building GSD SDK from source (${sdkDir})…${reset}`);
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
// 1. Install sdk build-time dependencies (tsc, etc.)
const installResult = spawnSync(npmCmd, ['install'], { cwd: sdkDir, stdio: 'inherit' });
if (installResult.status !== 0) {
warnManual('Failed to `npm install` in sdk/.');
return;
}
// 2. Compile TypeScript → sdk/dist/
const buildResult = spawnSync(npmCmd, ['run', 'build'], { cwd: sdkDir, stdio: 'inherit' });
if (buildResult.status !== 0) {
warnManual('Failed to `npm run build` in sdk/.');
return;
}
// 3. Install the built package globally so `gsd-sdk` lands on PATH.
const globalResult = spawnSync(npmCmd, ['install', '-g', '.'], { cwd: sdkDir, stdio: 'inherit' });
if (globalResult.status !== 0) {
warnManual('Failed to `npm install -g .` from sdk/.');
return;
}
// Verify gsd-sdk is actually resolvable on PATH. npm's global bin dir is
// not always on the current shell's PATH (Homebrew prefixes, nvm setups,
// unconfigured npm prefix), so a zero exit status from `npm install -g`
// alone is not proof of a working binary.
const resolverCmd = process.platform === 'win32' ? 'where' : 'which';
const verify = spawnSync(resolverCmd, ['gsd-sdk'], { encoding: 'utf-8' });
if (verify.status === 0 && verify.stdout && verify.stdout.trim()) {
console.log(` ${green}✓${reset} Built and installed GSD SDK from source (gsd-sdk resolved at ${verify.stdout.trim().split('\n')[0]})`);
} else {
warnManual('Built and installed GSD SDK from source but gsd-sdk is not on PATH — npm global bin may not be in your PATH.');
if (verify.stderr) console.warn(` resolver stderr: ${verify.stderr.trim()}`);
}
}
/**
* Install GSD for all selected runtimes
*/
@@ -6641,7 +6737,15 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
const primaryStatuslineResult = results.find(r => statuslineRuntimes.includes(r.runtime));
const finalize = (shouldInstallStatusline) => {
// Handle SDK installation before printing final summaries
// Build @gsd-build/sdk from the in-repo sdk/ source and install it globally
// so `gsd-sdk` lands on PATH. Every /gsd-* command shells out to
// `gsd-sdk query …`; without this, commands fail with "command not found:
// gsd-sdk". The npm-published @gsd-build/sdk is kept intentionally frozen
// at an older version; we always build from source so users get the SDK
// that matches the installed GSD version.
// Runs by default; skip with --no-sdk. Idempotent when already present.
installSdkIfNeeded();
const printSummaries = () => {
for (const result of results) {
const useStatusline = statuslineRuntimes.includes(result.runtime) && shouldInstallStatusline;

View File

@@ -11,7 +11,12 @@
"get-shit-done",
"agents",
"hooks",
"scripts"
"scripts",
"sdk/src",
"sdk/prompts",
"sdk/package.json",
"sdk/package-lock.json",
"sdk/tsconfig.json"
],
"keywords": [
"claude",

1
sdk/package-lock.json generated
View File

@@ -7,6 +7,7 @@
"": {
"name": "@gsd-build/sdk",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.84",
"ws": "^8.20.0"

View File

@@ -309,8 +309,10 @@ export class GSDEventStream extends EventEmitter {
): GSDEvent | null {
const events: GSDEvent[] = [];
// Extract text blocks — content blocks are a discriminated union with a 'type' field
const content = msg.message.content as Array<{ type: string; [key: string]: unknown }>;
// Extract text blocks — content blocks are a discriminated union with a 'type' field.
// Double-cast via unknown because BetaContentBlock's internal variants don't
// carry an index signature, so TS rejects the direct cast without a widening step.
const content = msg.message.content as unknown as Array<{ type: string; [key: string]: unknown }>;
const textBlocks = content.filter(
(b): b is { type: 'text'; text: string } => b.type === 'text',

View File

@@ -48,22 +48,67 @@ describe('#1656: community .sh hooks must be present in hooks/dist', () => {
});
// ─── #1657 ───────────────────────────────────────────────────────────────────
//
// Historical context: #1657 originally guarded against a broken `promptSdk()`
// flow that shipped when `@gsd-build/sdk` did not yet exist on npm. The
// package was published at v0.1.0 and is now a hard runtime requirement for
// every /gsd-* command (they all shell out to `gsd-sdk query …`).
//
// #2385 restored the `--sdk` flag and made SDK install the default path in
// bin/install.js. These guards are inverted: we now assert that SDK install
// IS wired up, and that the old broken `promptSdk()` prompt is still gone.
describe('#1657: SDK prompt must not appear in installer source', () => {
describe('#1657 / #2385: SDK install must be wired into installer source', () => {
let src;
test('install.js does not contain promptSdk call', () => {
test('install.js does not contain the legacy promptSdk() prompt (#1657)', () => {
src = fs.readFileSync(INSTALL_SRC, 'utf-8');
assert.ok(
!src.includes('promptSdk('),
'promptSdk() must not be called — SDK prompt causes install failures when package does not exist on npm'
'promptSdk() must not be reintroduced — the old interactive prompt flow was broken'
);
});
test('install.js does not contain --sdk flag handling', () => {
test('install.js wires up --sdk / --no-sdk flag handling (#2385)', () => {
src = src || fs.readFileSync(INSTALL_SRC, 'utf-8');
assert.ok(
!src.includes("args.includes('--sdk')"),
'--sdk flag must be removed to prevent users triggering a broken SDK install'
src.includes("args.includes('--sdk')"),
'--sdk flag must be parsed so users can force SDK (re)install'
);
assert.ok(
src.includes("args.includes('--no-sdk')"),
'--no-sdk flag must be parsed so users can opt out of SDK install'
);
});
test('install.js builds gsd-sdk from in-repo sdk/ source (#2385)', () => {
src = src || fs.readFileSync(INSTALL_SRC, 'utf-8');
// The installer must locate the in-repo sdk/ directory, run the build,
// and install it globally. We intentionally do NOT install
// @gsd-build/sdk from npm because that published version lags the source
// tree and shipping it breaks query handlers added since the last
// publish.
assert.ok(
src.includes("path.resolve(__dirname, '..', 'sdk')") ||
src.includes('path.resolve(__dirname, "..", "sdk")'),
'installer must locate the in-repo sdk/ directory'
);
assert.ok(
src.includes("'npm install -g .'") ||
src.includes("['install', '-g', '.']"),
'installer must run `npm install -g .` from sdk/ to install the built package globally'
);
assert.ok(
src.includes("['run', 'build']"),
'installer must compile TypeScript via `npm run build` before installing globally'
);
});
test('package.json ships sdk source in published tarball (#2385)', () => {
const rootPkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
const files = rootPkg.files || [];
assert.ok(
files.some((f) => f === 'sdk' || f.startsWith('sdk/')),
'root package.json `files` must include sdk source so npm-registry installs can build gsd-sdk from source'
);
});
});