chore(#604): rename get-shit-done/ runtime directory to gsd-core/ (#615)

* chore(#604): rename get-shit-done/ runtime directory to gsd-core/

Renames the installed runtime directory `get-shit-done/` to `gsd-core/` so the
on-disk name matches the package (`@opengsd/gsd-core`), repo, and binary
(`gsd-tools`). The npm package name and binary are unchanged; npx/npm consumers
are unaffected.

Mechanical (bulk, ~90% of the diff):
- `git mv get-shit-done gsd-core`
- Swept path/identifier references across the repo via
  `perl -pe 's/get-shit-done(?!-\w)/gsd-core/g'`. The negative lookahead
  preserves the five legitimate slug variants that are NOT the directory:
  get-shit-done-{OLD,cc,classic,cli,redux} (old package/repo names).
- Build/manifest wiring: package.json (bin, files, coverage globs),
  tsconfig.build.json (outDir), ~86 .gitignore build-output entries,
  stryker.config.mjs, scan-ignore files, install.js path strings.
- Frozen (not rewritten): CHANGELOG.md history; translated docs
  (README.<locale>.md and docs/{ja-JP,ko-KR,pt-BR,zh-CN}/).

New logic (review here):
- src/installer-migrations/003-rename-get-shit-done-to-gsd-core.cts: a proper
  ADR-0008 installer migration. On upgrade it walks the legacy
  `~/.claude/get-shit-done/` tree, classifies each file via the prior install
  manifest, and emits remove-managed / backup-and-remove for managed files
  while PRESERVING unknown user-added files. Symlink-safe (skips a symlinked
  root and symlinked entries; bounds-checks every path under configDir). The
  framework rolls back on install failure. Emptied dirs may remain (framework
  has no recursive dir-removal primitive) — documented.
- scripts/lint-legacy-dir-name.cjs: CI regression guard forbidding the bare
  `get-shit-done` directory token (split token to avoid self-match; case-
  insensitive; `(?!-\w)` lookahead allows the slug variants; allowlists
  CHANGELOG, translated docs, and `gsd-allow-legacy-name` marker lines).
  Wired into the lint-tests CI job.
- Restored scripts/lint-package-identity-drift.cjs detection regexes (the
  mechanical sweep had wrongly rewritten the old-name patterns it exists to
  detect) and marked them as intentional legacy references.
- TDD tests for the migration and the guard; do.md slash-command guard regex
  tightened so a `/gsd-core/bin` path segment is not mistaken for a command;
  changeset + docs/installer-migrations.md row added.

Breaking: the installed runtime path moves `~/.claude/get-shit-done/` ->
`~/.claude/gsd-core/`. Migration 003 removes the stale legacy dir's managed
files (preserving user files) on upgrade. Users with custom hooks/configs
hardcoding the old path must update them.

Closes #604

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#604): unsweep pending changesets + allowlist injection-example docs

CI fixes for the rename PR:
- Do not sweep pending .changeset/*.md (ephemeral release-note fragments,
  like CHANGELOG); reverted those body edits so 5 pre-existing malformed
  fragments (missing type/pr) no longer enter the PR diff and trip docs-lint.
  Allowlisted .changeset/ in the legacy-name guard accordingly.
- Allowlisted TEST-EXAMPLES.md and docs/explanation/security-model.md in
  prompt-injection-scan.sh: they contain intentional injection examples /
  security-model prose; the path-reference rewrites are kept.

CodeQL alerts on this PR are pre-existing (alert lines unchanged by this PR;
none in the new migration/guard) and are out of scope for the rename.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#604): resolve CodeQL alerts surfaced on this PR

The rename diff touched files carrying pre-existing CodeQL findings; per the
no-pre-existing-dismissal rule, fixing every surfaced alert rather than waving
them off. All behavior-preserving:

- scripts/ci-test-scope.cjs: build the config-path match from string
  .includes() instead of a RegExp over an arg-derived value (js/regex-injection).
- src/profile-output.cts: escape backslashes before pipe-escaping desc/safeName
  so the table-cell escape is complete (js/incomplete-sanitization).
- tests/{bug-2643,bug-2808,docs-parity-live-registry}: two-pass HTML-comment
  strip so a bare/unclosed `<!--` cannot survive (js/incomplete-multi-character-sanitization).
- tests/inline-plan-threshold: drop the no-op `\s`->`\s` identity replace,
  keep the meaningful POSIX-class conversion (js/identity-replacement).

Verified: build:lib green; the touched test files + ci-test-scope + profile-output
suites pass; lint:legacy-name clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#604): correctly resolve remaining CodeQL alerts (regex-injection + sanitization)

The prior commit's fixes for two alerts were ineffective:
- ci-test-scope.cjs js/regex-injection: the alert is the CLI-arg-derived `file`
  reaching static regex `.test(file)` calls (not the config rule). Removed ALL
  regex over file/t — startsWith/includes/=== string checks + an isWindowsHint
  helper — so there is no regex sink for the tainted value.
- js/incomplete-multi-character-sanitization (3 test files): a single
  `.replace(/<!--...-->/g,'')` can let `<!--` re-form. Replaced with a fixpoint
  loop (replace until stable) plus a final bare-opener strip.

Verified: no regex over file/t remains; ci-test-scope + the 3 test suites pass;
lint:legacy-name clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#604): make ci-test-scope + comment-strippers regex-free to clear CodeQL

CodeQL flags the regex PATTERNS syntactically (regex-injection on the
--files arg split; incomplete-multi-character-sanitization on the <!--...-->
replace), so loop fixes do not satisfy it. Made these paths regex-free:
- ci-test-scope.cjs splitFiles: char-by-char separator tokenizer (no /[,\\s]+/).
- 3 test files: indexOf/slice HTML-comment stripper (no .replace(/<!--/)).
Behavior preserved; ci-test-scope + the 3 suites pass; guard clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#604): unblock security base64 scan on the large rename diff

The security job hit its 10m timeout: base64-scan.sh choked on the binary
test fixture tests/feat-3594-parser-property-style.test.cjs (embedded NUL/
non-UTF8 bytes -> thousands of bogus blobs + "ignored null byte" warnings),
and the ~800-file rename diff is slow to scan regardless.

- scripts/base64-scan.sh: skip binary-by-content files (grep -Iq .) — they
  can't carry base64-obfuscated *text* and feeding NUL bytes through the
  per-line scanner is pathologically slow. collect_files already filtered
  binary *extensions*; this catches binary *content* in text extensions.
- .github/workflows/security-scan.yml: raise the security job timeout 10m->30m
  to accommodate very large diffs (the scan itself is unchanged).

Verified locally: scan skips the fixture, 0 "ignored null byte" warnings,
0 findings, exit 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#604): sweep get-shit-done refs introduced by merging next

The branch was updated with next (#614/#384/#618 etc.), which reference the
get-shit-done/ dir (still named that on next). Swept the stale references in
the merged files to gsd-core so the rename stays consistent and lint:legacy-name
passes:
- commands/gsd/discuss-phase.md (runtime-launcher shim paths)
- src/core.cts (getAgentsDir layout comments)
- tests/bug-384-agents-runtime-aware.test.cjs (require path to runtime lib)

Verified: guard 0 violations; build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#604): exclude gsd-core/ path segments from bug-3683 command cross-ref invariant

The #614 runtime-launcher shim added to discuss-phase.md references
`${_GSD_RUNTIME_ROOT}/gsd-core/bin/...`. bug-3683's REF_PATTERN excluded path-y
refs only via lookbehind, but `}` precedes `/gsd-core/` in the shim, so it
mis-read the directory path as a dangling `/gsd-core` command ref (same class as
the #604 bug-2954 fix). Added a trailing `(?![\w-]*\/)` so `/gsd-<x>/...` path
segments are not treated as slash-command references.

Verified locally on BOTH platforms before pushing:
- mac (node 26) full suite: 0 failures
- gsd-test-runner (linux, node22 image) full suite: 0 failures
- bug-3683 + bug-2954 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#604): lazily resolve findProjectRoot in gsd-tools (harden flaky CI)

CI intermittently failed state.test's gsd-tools subprocess with
"findProjectRoot is not a function" (flip-flopping across legs; not reproducible
on mac full suite, gsd-test linux full suite, test:unit, or state.test x8).
findProjectRoot is a re-export from core.cjs (sourced from project-root.cjs);
binding it via destructure at module-load can be undefined under a load-ordering
edge. Resolve it lazily at call time via a small wrapper so the lookup happens
after core.cjs is fully initialized.

Verified green on BOTH platforms before pushing:
- mac (node 26) full suite: 0 failures
- gsd-test-runner (linux, node22) full suite: 0 failures
- state.test.cjs: 106/106; gsd-tools loads cleanly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#604): allowlist verification-patterns.md placeholder examples in secret scan

The rename git-mv'd references/verification-patterns.md into gsd-core/, pulling
it into the secret-scan diff. It documents stub/placeholder RED-FLAG env-var
examples (illustrative Stripe test-key / database-URL / API-key placeholders) —
not real credentials. Added it to .secretscanignore with the strict annotation,
mirroring the existing gsd-core/workflows/plan-phase.md exception.

Verified locally: secret-scan-lint --strict OK; secret-scan --diff origin/next
exits 0 with 0 findings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-02 18:35:29 -04:00
committed by GitHub
parent b177c1704f
commit 463cffd894
845 changed files with 3316 additions and 2373 deletions

View File

@@ -25,7 +25,7 @@ const PR_FULL_SUITES = ['unit', 'integration', 'security'];
// Relative to repoRoot. We walk these to discover SUT-internal requires so that
// a change to a deep helper propagates through re-export chains to tests.
const SOURCE_TREES = [
'get-shit-done/bin/lib',
'gsd-core/bin/lib',
'bin/lib',
'bin',
'scripts',

View File

@@ -217,6 +217,18 @@ extract_and_check_blobs() {
local found=0
local line_num=0
# Skip binary-by-content files (e.g. adversarial parser fixtures with embedded
# non-UTF8 / NUL bytes). They cannot meaningfully carry base64-obfuscated *text*,
# and feeding NUL bytes through the per-line scanner spawns thousands of bogus
# `base64 -d` subprocesses (the "ignored null byte" warnings) — slow enough to
# blow the job timeout on a large diff. `grep -Iq .` treats a binary file as
# non-matching, so this skips it with a coverage notice (collect_files already
# filters binary *extensions*; this catches binary *content* in text extensions).
if ! LC_ALL=C grep -Iq . "$file" 2>/dev/null; then
echo "SKIP: $file (binary content — base64 text scan not applicable)" >&2
return 0
fi
while IFS= read -r line; do
line_num=$((line_num + 1))

View File

@@ -24,8 +24,8 @@ const { renderGithubReleaseNotes } = require('./github-release-notes.cjs');
const {
compareSemverCore,
isStableTripletSemver,
} = require('../../get-shit-done/bin/lib/semver-compare.cjs');
const { packageName, repoSlug: defaultRepoSlug } = require('../../get-shit-done/bin/lib/package-identity.cjs');
} = require('../../gsd-core/bin/lib/semver-compare.cjs');
const { packageName, repoSlug: defaultRepoSlug } = require('../../gsd-core/bin/lib/package-identity.cjs');
function parseArgs(argv) {
const opts = {

View File

@@ -4,7 +4,7 @@ const cp = require('node:child_process');
const path = require('node:path');
const { parseFragment } = require('./parse.cjs');
const { packageName, repoSlug: defaultRepoSlug } = require('../../get-shit-done/bin/lib/package-identity.cjs');
const { packageName, repoSlug: defaultRepoSlug } = require('../../gsd-core/bin/lib/package-identity.cjs');
const SECTION_ORDER = ['Fixed', 'Added', 'Changed', 'Deprecated', 'Removed', 'Security'];

View File

@@ -25,7 +25,7 @@ const OPT_OUT_LABEL = 'no-changelog';
// fragment or an explicit opt-out label. Test/CI/docs/lock files do not.
const USER_FACING_PREFIXES = [
'bin/',
'get-shit-done/',
'gsd-core/',
'agents/',
'commands/',
'hooks/',

View File

@@ -4,7 +4,7 @@ const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.resolve(__dirname, '..');
const aliasesPath = path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'command-aliases.cjs');
const aliasesPath = path.join(ROOT, 'gsd-core', 'bin', 'lib', 'command-aliases.cjs');
function fail(message) {
process.stderr.write(`${message}\n`);
@@ -37,37 +37,37 @@ const families = [
{
commandAliases: 'STATE_COMMAND_ALIASES',
subcommands: 'STATE_SUBCOMMANDS',
routerPath: path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'state-command-router.cjs'),
routerPath: path.join(ROOT, 'gsd-core', 'bin', 'lib', 'state-command-router.cjs'),
},
{
commandAliases: 'VERIFY_COMMAND_ALIASES',
subcommands: 'VERIFY_SUBCOMMANDS',
routerPath: path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'verify-command-router.cjs'),
routerPath: path.join(ROOT, 'gsd-core', 'bin', 'lib', 'verify-command-router.cjs'),
},
{
commandAliases: 'INIT_COMMAND_ALIASES',
subcommands: 'INIT_SUBCOMMANDS',
routerPath: path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'init-command-router.cjs'),
routerPath: path.join(ROOT, 'gsd-core', 'bin', 'lib', 'init-command-router.cjs'),
},
{
commandAliases: 'PHASE_COMMAND_ALIASES',
subcommands: 'PHASE_SUBCOMMANDS',
routerPath: path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'phase-command-router.cjs'),
routerPath: path.join(ROOT, 'gsd-core', 'bin', 'lib', 'phase-command-router.cjs'),
},
{
commandAliases: 'PHASES_COMMAND_ALIASES',
subcommands: 'PHASES_SUBCOMMANDS',
routerPath: path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'phases-command-router.cjs'),
routerPath: path.join(ROOT, 'gsd-core', 'bin', 'lib', 'phases-command-router.cjs'),
},
{
commandAliases: 'VALIDATE_COMMAND_ALIASES',
subcommands: 'VALIDATE_SUBCOMMANDS',
routerPath: path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'validate-command-router.cjs'),
routerPath: path.join(ROOT, 'gsd-core', 'bin', 'lib', 'validate-command-router.cjs'),
},
{
commandAliases: 'ROADMAP_COMMAND_ALIASES',
subcommands: 'ROADMAP_SUBCOMMANDS',
routerPath: path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'roadmap-command-router.cjs'),
routerPath: path.join(ROOT, 'gsd-core', 'bin', 'lib', 'roadmap-command-router.cjs'),
},
];

View File

@@ -44,7 +44,7 @@ const RULES = [
},
{
name: 'TS runtime sources (ADR-457 build-at-publish)',
// src/*.cts compiles into get-shit-done/bin/lib/*.cjs; a source-only edit must
// src/*.cts compiles into gsd-core/bin/lib/*.cjs; a source-only edit must
// still trigger the migrated module's tests (otherwise CI silently skips them).
match: path => path.startsWith('src/') || path === 'tsconfig.build.json',
tests: [
@@ -55,7 +55,7 @@ const RULES = [
{
name: 'installer and package layout',
match: path => path.startsWith('bin/') ||
path.startsWith('get-shit-done/bin/') ||
path.startsWith('gsd-core/bin/') ||
path.includes('install') ||
path.includes('release-tarball-smoke'),
fullMatrix: true,
@@ -122,7 +122,7 @@ const RULES = [
},
{
name: 'workflow prompts',
match: path => path.startsWith('get-shit-done/workflows/'),
match: path => path.startsWith('gsd-core/workflows/'),
tests: [
'tests/workflow-compat.test.cjs',
'tests/workflow-size-budget.test.cjs',
@@ -153,7 +153,7 @@ const RULES = [
},
{
name: 'configuration',
match: path => /config|configuration|model-catalog|model-profile/.test(path),
match: path => ['config', 'configuration', 'model-catalog', 'model-profile'].some(k => path.includes(k)),
tests: [
'tests/config.test.cjs',
'tests/config-get-default.test.cjs',
@@ -208,7 +208,19 @@ function parseArgs(argv) {
function splitFiles(value) {
if (!value) return [];
return value.split(/[,\s]+/).map(v => v.trim()).filter(Boolean);
const SEPARATORS = new Set([',', ' ', '\t', '\n', '\r', '\f', '\v']);
const tokens = [];
let current = '';
for (const ch of value) {
if (SEPARATORS.has(ch)) {
if (current) tokens.push(current);
current = '';
} else {
current += ch;
}
}
if (current) tokens.push(current);
return tokens.map(v => v.trim()).filter(Boolean);
}
function changedFiles(args) {
@@ -231,6 +243,9 @@ function addAll(set, values) {
for (const value of values) set.add(value);
}
const WINDOWS_HINTS = ['windows', 'path', 'shell', 'workflow', 'install', 'hook'];
const isWindowsHint = s => WINDOWS_HINTS.some(k => s.toLowerCase().includes(k));
function classify(files) {
const targeted = new Set();
const windows = new Set();
@@ -239,9 +254,9 @@ function classify(files) {
let fullMatrix = false;
for (const file of files) {
if (/^(bin|get-shit-done|agents|commands|docs|hooks|tests|scripts)\//.test(file) ||
/^package(-lock)?\.json$/.test(file) ||
/^tsconfig.*\.json$/.test(file) ||
if (['bin/', 'gsd-core/', 'agents/', 'commands/', 'docs/', 'hooks/', 'tests/', 'scripts/'].some(p => file.startsWith(p)) ||
file === 'package.json' || file === 'package-lock.json' ||
(file.startsWith('tsconfig') && file.endsWith('.json')) ||
file.startsWith('.github/workflows/') ||
file.startsWith('.github/rulesets/')) {
codeChanged = true;
@@ -250,7 +265,7 @@ function classify(files) {
if (file.startsWith('tests/') && file.endsWith('.test.cjs')) {
targeted.add(file);
fullMatrix = true;
if (/windows|path|shell|workflow|install|hook/i.test(file)) {
if (isWindowsHint(file)) {
windows.add(file);
}
}
@@ -272,7 +287,7 @@ function classify(files) {
targetedTests.push('unit');
}
const windowsTests = existingTests([...new Set([...windows, ...targetedTests.filter(t => /windows|path|shell|workflow|install|hook/i.test(t))])].sort());
const windowsTests = existingTests([...new Set([...windows, ...targetedTests.filter(isWindowsHint)])].sort());
return {
code_changed: codeChanged,

View File

@@ -54,7 +54,7 @@ function executionContextRefs(content) {
const trailingProse = line.length > token.length;
const normalized = token
.replace(/^@(?:~|\$HOME)\//, '')
.replace(/^(?:\.claude\/)?(?:get-shit-done\/)?/, '');
.replace(/^(?:\.claude\/)?(?:gsd-core\/)?/, '');
refs.push({ token, normalized, trailingProse });
}
}

View File

@@ -20,11 +20,11 @@ const path = require('node:path');
const COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
const SEARCH_DIRS = [
path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib'),
path.join(__dirname, '..', 'get-shit-done', 'workflows'),
path.join(__dirname, '..', 'get-shit-done', 'references'),
path.join(__dirname, '..', 'get-shit-done', 'templates'),
path.join(__dirname, '..', 'get-shit-done', 'contexts'),
path.join(__dirname, '..', 'gsd-core', 'bin', 'lib'),
path.join(__dirname, '..', 'gsd-core', 'workflows'),
path.join(__dirname, '..', 'gsd-core', 'references'),
path.join(__dirname, '..', 'gsd-core', 'templates'),
path.join(__dirname, '..', 'gsd-core', 'contexts'),
path.join(__dirname, '..', 'commands', 'gsd'),
path.join(__dirname, '..', 'agents'),
path.join(__dirname, '..', 'hooks'),

View File

@@ -34,19 +34,19 @@ const FAMILIES = [
},
{
name: 'workflows',
dir: path.join(ROOT, 'get-shit-done', 'workflows'),
dir: path.join(ROOT, 'gsd-core', 'workflows'),
filter: (f) => f.endsWith('.md'),
toName: (f) => f,
},
{
name: 'references',
dir: path.join(ROOT, 'get-shit-done', 'references'),
dir: path.join(ROOT, 'gsd-core', 'references'),
filter: (f) => f.endsWith('.md'),
toName: (f) => f,
},
{
name: 'cli_modules',
dir: path.join(ROOT, 'get-shit-done', 'bin', 'lib'),
dir: path.join(ROOT, 'gsd-core', 'bin', 'lib'),
filter: (f) => f.endsWith('.cjs'),
toName: (f) => f,
},

View File

@@ -6,7 +6,7 @@
*
* `deriveIdentity(pkg)` is the pure core: it turns a parsed package.json into
* the coordinate record every consumer needs. The generated runtime module
* `get-shit-done/bin/lib/package-identity.cjs` bakes those values at build
* `gsd-core/bin/lib/package-identity.cjs` bakes those values at build
* time, because the installed tree carries only a synthetic
* `{"type":"commonjs"}` package.json (no `.name`) — so a runtime
* `require('package.json').name` resolves to `undefined` (the #378 bug this
@@ -115,7 +115,7 @@ function main() {
const fs = require('node:fs');
const path = require('node:path');
const pkg = require(path.join(__dirname, '..', 'package.json'));
const out = path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib', 'package-identity.cjs');
const out = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'package-identity.cjs');
fs.writeFileSync(out, render(deriveIdentity(pkg)));
process.stdout.write(`wrote ${path.relative(path.join(__dirname, '..'), out)}\n`);
}

View File

@@ -20,7 +20,7 @@ const path = require('path');
const ROOT = path.join(__dirname, '..');
const COMMANDS_DIR = path.join(ROOT, 'commands', 'gsd');
const GSD_ROOT = path.join(ROOT, 'get-shit-done');
const GSD_ROOT = path.join(ROOT, 'gsd-core');
const {
CANONICAL_TOOLS,

View File

@@ -0,0 +1,156 @@
#!/usr/bin/env node
/**
* lint-legacy-dir-name.cjs
*
* Prevents accidental re-introduction of the bare legacy directory token
* `get-shit-done` now that the package has been renamed to `gsd-core` (#604).
*
* The forbidden token is constructed by splitting across the concat operator
* so this guard script cannot flag itself:
* const FORBIDDEN = 'get-shit' + '-done';
*
* Regex: FORBIDDEN + '(?!-\\w)' — allows any hyphenated slug variant
* (get-shit-done-OLD, get-shit-done-classic, get-shit-done-cli, etc.)
* while forbidding the bare word boundary that would indicate a stale
* directory reference. This is the exact pattern that would appear in a
* path like `~/.claude/get-shit-done/` or `'get-shit-done'` in code.
*
* Allowlist:
* - CHANGELOG.md (historical record; reviewed manually)
* - .changeset/ (ephemeral release-note fragments; consumed into CHANGELOG on
* release — like CHANGELOG, not swept by rename PRs)
* - Translated READMEs: README.ja-JP.md, README.ko-KR.md, README.pt-BR.md, README.zh-CN.md
* - Locale-specific docs dirs: docs/ja-JP/, docs/ko-KR/, docs/pt-BR/, docs/zh-CN/
* - Lines containing the marker `gsd-allow-legacy-name` (intentional uses)
* - Binary files (detected by NUL byte scan)
* - This guard script itself (by path)
*
* Exit 0 if no violations; exit 1 if any are found (with stderr diagnostics).
*/
'use strict';
const { execFileSync } = require('child_process');
const fs = require('fs');
const path = require('path');
// Constructed with split to avoid self-match when this script is scanned.
const FORBIDDEN = 'get-shit' + '-done';
const FORBIDDEN_RE = new RegExp(FORBIDDEN + '(?!-\\w)', 'gi');
const ALLOW_MARKER = 'gsd-allow-legacy-name';
const SELF_PATH = path.resolve(__filename);
// GSD_LINT_LEGACY_REPO_ROOT is used by tests to redirect the guard to a
// temporary fixture git repo without touching the real working tree.
const REPO_ROOT = process.env.GSD_LINT_LEGACY_REPO_ROOT
? path.resolve(process.env.GSD_LINT_LEGACY_REPO_ROOT)
: path.resolve(__dirname, '..');
const ALLOWLIST_FILES = new Set([
'CHANGELOG.md',
'README.ja-JP.md',
'README.ko-KR.md',
'README.pt-BR.md',
'README.zh-CN.md',
]);
const ALLOWLIST_DIR_PREFIXES = [
// Pending changeset fragments are ephemeral release-note stubs consumed into
// CHANGELOG on release — like CHANGELOG itself, they should not be swept by
// rename PRs and may legitimately contain the legacy token in historical prose.
'.changeset/',
'docs/ja-JP/',
'docs/ko-KR/',
'docs/pt-BR/',
'docs/zh-CN/',
];
function isAllowlisted(relPath) {
if (ALLOWLIST_FILES.has(relPath)) return true;
for (const prefix of ALLOWLIST_DIR_PREFIXES) {
if (relPath.startsWith(prefix)) return true;
}
return false;
}
function isBinary(fullPath) {
// Read a small chunk and check for NUL bytes.
let fd;
try {
fd = fs.openSync(fullPath, 'r');
const buf = Buffer.allocUnsafe(512);
const bytesRead = fs.readSync(fd, buf, 0, 512, 0);
return buf.subarray(0, bytesRead).includes(0);
} catch {
return false;
} finally {
if (fd != null) {
try { fs.closeSync(fd); } catch { /* best-effort */ }
}
}
}
// Enumerate tracked files via git ls-files so only committed/staged source is checked.
let trackedFiles;
try {
trackedFiles = execFileSync('git', ['ls-files'], { cwd: REPO_ROOT, encoding: 'utf8' })
.split('\n')
.map((f) => f.trim())
.filter(Boolean);
} catch (err) {
process.stderr.write('ERROR lint-legacy-dir-name: git ls-files failed: ' + err.message + '\n');
process.exit(1);
}
const violations = [];
for (const relPath of trackedFiles) {
if (isAllowlisted(relPath)) continue;
const fullPath = path.join(REPO_ROOT, relPath);
// Skip this guard script itself.
if (path.resolve(fullPath) === SELF_PATH) continue;
if (isBinary(fullPath)) continue;
let content;
try {
content = fs.readFileSync(fullPath, 'utf8');
} catch {
// Unreadable files (permissions, etc.) — skip silently.
continue;
}
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Skip lines that carry the explicit allow marker.
if (line.includes(ALLOW_MARKER)) continue;
FORBIDDEN_RE.lastIndex = 0;
let match;
while ((match = FORBIDDEN_RE.exec(line)) !== null) {
violations.push({
file: relPath,
line: i + 1,
col: match.index + 1,
text: match[0],
});
}
}
}
if (violations.length === 0) {
process.stdout.write('ok lint-legacy-dir-name: ' + trackedFiles.length + ' tracked file(s) checked, 0 violations\n');
process.exit(0);
}
process.stderr.write('\nERROR lint-legacy-dir-name: ' + violations.length + ' violation(s) found\n\n');
for (const v of violations) {
process.stderr.write(' ' + v.file + ':' + v.line + ':' + v.col + ' — ' + JSON.stringify(v.text) + '\n');
}
process.stderr.write('\n');
process.stderr.write('Fix: rename to gsd-core, or add `gsd-allow-legacy-name` marker on the line if the\n');
process.stderr.write(' use is intentional (migration modules, tests, guard, changeset).\n\n');
process.exit(1);

View File

@@ -8,7 +8,7 @@
* not that the runtime behavior is correct.
*
* ALLOWED:
* - require('../get-shit-done/bin/lib/foo.cjs') -- runs the module, not text inspection
* - require('../gsd-core/bin/lib/foo.cjs') -- runs the module, not text inspection
* - readFileSync on .md / .json / .txt files -- product-content or config output
* - Files annotated: // allow-test-rule: <reason>
*
@@ -28,16 +28,16 @@ const TESTS_DIR = path.join(__dirname, '..', 'tests');
const ALLOW_ANNOTATION = /\/\/\s*allow-test-rule:\s*\S/;
// Matches constant definitions that hold a .cjs path in a SOURCE directory.
// Requires a source-dir indicator ('bin', 'lib', 'get-shit-done') to avoid
// Requires a source-dir indicator ('bin', 'lib', 'gsd-core') to avoid
// flagging temp files like path.join(tmpDir, 'example.cjs').
// const CONFIG_PATH = path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib', 'config-schema.cjs');
const CJS_PATH_CONST_RE = /(?:const|let|var)\s+(\w+)\s*=\s*path\.join\s*\([^)]*(?:'bin'|"bin"|'lib'|"lib"|'get-shit-done'|"get-shit-done")[^)]*['"][^'"]*\.cjs['"]/gm;
// const CONFIG_PATH = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'config-schema.cjs');
const CJS_PATH_CONST_RE = /(?:const|let|var)\s+(\w+)\s*=\s*path\.join\s*\([^)]*(?:'bin'|"bin"|'lib'|"lib"|'gsd-core'|"gsd-core")[^)]*['"][^'"]*\.cjs['"]/gm;
// Matches readFileSync with a named variable as first arg
const READ_WITH_CONST_RE = /readFileSync\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*,/gm;
// Matches readFileSync with an inline path.join(.cjs) as first arg
const READ_WITH_INLINE_CJS_RE = /readFileSync\s*\([^,)]*path\.join\s*\([^)]*(?:'bin'|"bin"|'lib'|"lib"|'get-shit-done'|"get-shit-done")[^)]*['"][^'"]*\.cjs['"]/;
const READ_WITH_INLINE_CJS_RE = /readFileSync\s*\([^,)]*path\.join\s*\([^)]*(?:'bin'|"bin"|'lib'|"lib"|'gsd-core'|"gsd-core")[^)]*['"][^'"]*\.cjs['"]/;
/**
* #2962-class violations: raw text matching against process output or file

View File

@@ -4,7 +4,7 @@
/**
* Drift-guard lint for the Package Identity seam (issue #498).
*
* The seam (`get-shit-done/bin/lib/package-identity.cjs`, derived from
* The seam (`get-shit-done/bin/lib/package-identity.cjs`, derived from // gsd-allow-legacy-name
* package.json) is the single source of GSD's published coordinates. Many
* runtime surfaces still carry a literal copy of those coordinates because
* they cannot `require()` the seam at runtime: the bash launcher snippet (and
@@ -19,7 +19,7 @@
* literal is reported until updated. That is what turns a repoint into a
* one-line change with mechanical enforcement.
*
* Scope: the runtime/code surface (bin/, hooks/, scripts/, get-shit-done/).
* Scope: the runtime/code surface (bin/, hooks/, scripts/, get-shit-done/). // gsd-allow-legacy-name
* Pure-prose docs and localized READMEs are intentionally out of scope.
*/
@@ -27,11 +27,11 @@ const fs = require('node:fs');
const path = require('node:path');
// A GSD package coordinate: a scoped npm name whose package part contains
// "get-shit-done" (so @opengsd/gsd-sdk and unrelated scopes never match).
const PACKAGE_RE = /@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]*get-shit-done[A-Za-z0-9._-]*/g;
// "get-shit-done" (so @opengsd/gsd-sdk and unrelated scopes never match). // gsd-allow-legacy-name
const PACKAGE_RE = /@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]*get-shit-done[A-Za-z0-9._-]*/g; // gsd-allow-legacy-name
// A GSD repo slug, only inside a GitHub URL context so it never overlaps the
// scoped package literal above. The `.git` suffix is trimmed before compare.
const SLUG_RE = /(?:github\.com[/:]|raw\.githubusercontent\.com\/)([A-Za-z0-9._-]+\/[A-Za-z0-9._-]*get-shit-done[A-Za-z0-9._-]*)/g;
const SLUG_RE = /(?:github\.com[/:]|raw\.githubusercontent\.com\/)([A-Za-z0-9._-]+\/[A-Za-z0-9._-]*get-shit-done[A-Za-z0-9._-]*)/g; // gsd-allow-legacy-name
function lineOf(text, index) {
let line = 1;
@@ -62,14 +62,14 @@ function findCoordinateDrift(text, { packageName, repoSlug }) {
}
// Directories scanned, relative to repo root.
const SCAN_DIRS = ['bin', 'hooks', 'scripts', 'get-shit-done'];
const SCAN_DIRS = ['bin', 'hooks', 'scripts', 'gsd-core'];
const SCAN_EXT = new Set(['.js', '.cjs', '.sh', '.md']);
// Files exempt because they ARE the source of truth / the tooling that defines
// the coordinate patterns. The generated seam holds the correct value by
// construction; the generator and this lint carry regex/templates, not stray
// literals.
const EXEMPT = new Set([
path.join('get-shit-done', 'bin', 'lib', 'package-identity.cjs'),
path.join('gsd-core', 'bin', 'lib', 'package-identity.cjs'),
path.join('scripts', 'generate-package-identity.cjs'),
path.join('scripts', 'lint-package-identity-drift.cjs'),
]);
@@ -98,7 +98,7 @@ function walk(dir, acc) {
* annotated with the repo-relative file path.
*/
function scanRepo(root) {
const seam = require(path.join(root, 'get-shit-done', 'bin', 'lib', 'package-identity.cjs'));
const seam = require(path.join(root, 'gsd-core', 'bin', 'lib', 'package-identity.cjs'));
const expected = { packageName: seam.packageName, repoSlug: seam.repoSlug };
const violations = [];
for (const dir of SCAN_DIRS) {

View File

@@ -52,6 +52,6 @@ process.stderr.write(`ERROR shell-projection-drift: inline serialized shim build
for (const match of matches) {
process.stderr.write(` - ${match.label}\n`);
}
process.stderr.write('Route shim/wrapper rendering through get-shit-done/bin/lib/shell-command-projection.cjs\n');
process.stderr.write('Route shim/wrapper rendering through gsd-core/bin/lib/shell-command-projection.cjs\n');
process.stderr.write('Safe subprocess execution via spawnSync/execFileSync is intentionally allowed.\n');
process.exit(1);

View File

@@ -26,7 +26,7 @@
const fs = require('fs');
const path = require('path');
const PROFILES_MODULE = path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib', 'install-profiles.cjs');
const PROFILES_MODULE = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'install-profiles.cjs');
const { PROFILES, loadSkillsManifest, resolveProfile } = require(PROFILES_MODULE);
// ---------------------------------------------------------------------------

View File

@@ -2,7 +2,7 @@
/**
* lint-test-file-count.cjs — max 2 test files per production module.
*
* Scans sdk/src/query/, sdk/src/, get-shit-done/bin/lib/, bin/ for production
* Scans sdk/src/query/, sdk/src/, gsd-core/bin/lib/, bin/ for production
* modules, then counts matching test files in tests/ and sdk/src (recursive). Cap is 2
* (primary + one integration). Over-limit clusters must be in the allowlist with the
* EXACT set of test filenames grandfathered (identity ratchet via allowlist-ratchet.cjs).
@@ -22,7 +22,7 @@ const ROOT = path.join(__dirname, '..');
const PROD_DIRS = [
path.join(ROOT, 'sdk', 'src', 'query'),
path.join(ROOT, 'sdk', 'src'),
path.join(ROOT, 'get-shit-done', 'bin', 'lib'),
path.join(ROOT, 'gsd-core', 'bin', 'lib'),
path.join(ROOT, 'bin'),
];
const TEST_DIRS = [

View File

@@ -20,7 +20,7 @@
* "has_work": "true"|"false",
* "matrix": {
* "include": [
* { "name": "<module>", "mutate": "get-shit-done/bin/lib/<module>.cjs", "tests": "<space-joined test files>" },
* { "name": "<module>", "mutate": "gsd-core/bin/lib/<module>.cjs", "tests": "<space-joined test files>" },
* ...
* ]
* }
@@ -39,27 +39,27 @@ const { readFileSync } = require('fs');
// only ever produce survived mutants — so we scope strictly to these 6.
const COVERED = {
'context-utilization': {
cjs: 'get-shit-done/bin/lib/context-utilization.cjs',
cjs: 'gsd-core/bin/lib/context-utilization.cjs',
tests: [
'tests/context-utilization.property.test.cjs',
],
},
'prompt-budget': {
cjs: 'get-shit-done/bin/lib/prompt-budget.cjs',
cjs: 'gsd-core/bin/lib/prompt-budget.cjs',
tests: [
'tests/prompt-budget.property.test.cjs',
'tests/prompt-budget.unit.test.cjs',
],
},
frontmatter: {
cjs: 'get-shit-done/bin/lib/frontmatter.cjs',
cjs: 'gsd-core/bin/lib/frontmatter.cjs',
tests: [
'tests/frontmatter.property.test.cjs',
'tests/frontmatter.unit.test.cjs',
],
},
'adr-parser': {
cjs: 'get-shit-done/bin/lib/adr-parser.cjs',
cjs: 'gsd-core/bin/lib/adr-parser.cjs',
tests: [
'tests/adr-parser.property.test.cjs',
'tests/adr-parser.test.cjs',
@@ -67,13 +67,13 @@ const COVERED = {
],
},
'config-schema': {
cjs: 'get-shit-done/bin/lib/config-schema.cjs',
cjs: 'gsd-core/bin/lib/config-schema.cjs',
tests: [
'tests/config-schema.property.test.cjs',
],
},
'active-workstream-store': {
cjs: 'get-shit-done/bin/lib/active-workstream-store.cjs',
cjs: 'gsd-core/bin/lib/active-workstream-store.cjs',
tests: [
'tests/active-workstream-store.test.cjs',
'tests/active-workstream-store.unit.test.cjs',

View File

@@ -73,13 +73,17 @@ ALLOWLIST=(
'tests/security.test.cjs'
'tests/prompt-injection-scan.test.cjs'
'tests/verify.test.cjs'
'get-shit-done/bin/lib/security.cjs'
'gsd-core/bin/lib/security.cjs'
'hooks/gsd-prompt-guard.js'
'hooks/gsd-read-injection-scanner.js'
'tests/read-injection-scanner.test.cjs'
'tests/security-prompt-injection.test.cjs'
'tests/fixtures/adversarial/security/'
'SECURITY.md'
# These files contain intentional injection examples / security-model prose
# and are not attack vectors — they explain/demonstrate injection patterns.
'TEST-EXAMPLES.md'
'docs/explanation/security-model.md'
)
is_allowlisted() {

View File

@@ -33,7 +33,7 @@
* Non-interactive: --local --claude flags skip all prompts.
*
* Workflow-body checks (Cycle 3 — informational):
* - Scans all installed get-shit-done/workflows/*.md for /gsd:<known-cmd>
* - Scans all installed gsd-core/workflows/*.md for /gsd:<known-cmd>
* colon-namespace leaks (WORKFLOW_BODY_COLON_LEAK).
* This check populates result.details with counters but does NOT return a
* failure code by default; it is informational until enforcement is enabled.
@@ -45,7 +45,7 @@ const { execFileSync, spawnSync } = require('child_process');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { PACKAGE_NAME } = require('../get-shit-done/bin/lib/package-identity.cjs');
const { PACKAGE_NAME } = require('../gsd-core/bin/lib/package-identity.cjs');
// 120 s proved too tight on Windows GitHub-hosted runners: cold-cache
// `npm install -g` with a 1499-file tarball took ~120 s exactly, causing
// spawnSync to fire SIGTERM and return { status: null, stdout: '', stderr: '' }
@@ -182,15 +182,15 @@ function findInstallerBin(installPrefix) {
* Structured parser — only inspects individual lines; never regexes on the
* whole-file string. Two recognised forms (in priority order):
*
* 1. @-import line: `@~/.claude/get-shit-done/workflows/<name>.md`
* 2. Inline mention: any line containing `~/.claude/get-shit-done/workflows/<name>.md`
* 1. @-import line: `@~/.claude/gsd-core/workflows/<name>.md`
* 2. Inline mention: any line containing `~/.claude/gsd-core/workflows/<name>.md`
* (takes the LAST occurrence so conditional-dispatch files resolve to the
* default / unconditional branch, e.g. discuss-phase.md)
*
* Returns the bare workflow filename (e.g. `"discuss-phase.md"`) or null.
*/
function parseWorkflowRef(mdContent) {
const WORKFLOW_PREFIX = 'get-shit-done/workflows/';
const WORKFLOW_PREFIX = 'gsd-core/workflows/';
let atImportResult = null;
let lastInlineResult = null;
@@ -433,7 +433,7 @@ function runSmoke({
// Verify expected dirs were created
const expectedDirs = [
path.join(fixtureDir, '.claude', 'commands'),
path.join(fixtureDir, '.claude', 'get-shit-done'),
path.join(fixtureDir, '.claude', 'gsd-core'),
];
for (const dir of expectedDirs) {
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
@@ -472,9 +472,9 @@ function runSmoke({
let workflowPath = null;
if (workflowName) {
// Workflow files live at get-shit-done/workflows/<name> in the package.
// Workflow files live at gsd-core/workflows/<name> in the package.
// Some live in subdirectories; try flat first then scan once.
const flat = path.join(pkg, 'get-shit-done', 'workflows', workflowName);
const flat = path.join(pkg, 'gsd-core', 'workflows', workflowName);
workflowPath = fs.existsSync(flat) ? flat : null;
if (!workflowPath) {
@@ -503,7 +503,7 @@ function runSmoke({
// ─────────────────────────────────────────────────────────────────────────
// --- Workflow-body checks (informational — #3668 not yet fixed) ----------
const workflowsDir = path.join(pkg, 'get-shit-done', 'workflows');
const workflowsDir = path.join(pkg, 'gsd-core', 'workflows');
const installedCmdNames = readInstalledCmdNames(pkg);
let workflowsScanned = 0;

View File

@@ -26,7 +26,7 @@ const { execFileSync } = require('child_process');
const SUITES = ['all', 'unit', 'integration', 'install', 'security', 'slow'];
// ADR-457 build-at-publish: get-shit-done/bin/lib/*.cjs is generated from
// ADR-457 build-at-publish: gsd-core/bin/lib/*.cjs is generated from
// src/*.cts and gitignored, so on a clean checkout (fresh CI, before any build)
// the artifact is absent — yet test files require it. This is the universal
// chokepoint every test path funnels through (test:unit, --files-from, direct
@@ -38,7 +38,7 @@ const SUITES = ['all', 'unit', 'integration', 'install', 'security', 'slow'];
// modules migrate into src/.
function ensureBuiltArtifacts() {
const root = join(__dirname, '..');
const sentinel = join(root, 'get-shit-done', 'bin', 'lib', 'semver-compare.cjs');
const sentinel = join(root, 'gsd-core', 'bin', 'lib', 'semver-compare.cjs');
if (existsSync(sentinel)) return;
const tscBin = require.resolve('typescript/bin/tsc');
execFileSync(process.execPath, [tscBin, '-p', join(root, 'tsconfig.build.json')], {

View File

@@ -2,7 +2,7 @@
/**
* strip-prose-atrefs.cjs
*
* Removes redundant @~/.claude/get-shit-done/ path tokens from prose lines
* Removes redundant @~/.claude/gsd-core/ path tokens from prose lines
* in <process> and <context> blocks. The path is already declared in
* <execution_context> where it actually loads the file. Prose copies are
* inert and add ~900 tokens/invocation of dead weight.
@@ -30,7 +30,7 @@ const DRY_RUN = process.argv.includes('--dry-run');
const ROOT = path.join(__dirname, '..');
const COMMANDS_DIR = path.join(ROOT, 'commands', 'gsd');
const AT_PATH_PATTERN = /@(?:~|\$HOME)\/.+?get-shit-done\/[^\s`\)]+/;
const AT_PATH_PATTERN = /@(?:~|\$HOME)\/.+?gsd-core\/[^\s`\)]+/;
const mkAtRe = () => new RegExp(AT_PATH_PATTERN.source, 'g');
function transformLine(line) {

View File

@@ -2,7 +2,7 @@
/**
* sync-runtime-launcher.cjs
*
* Idempotent transform: for every get-shit-done/workflows/*.md (and subdirs),
* Idempotent transform: for every gsd-core/workflows/*.md (and subdirs),
* rewrite all bash/sh/shell fenced blocks to:
* 1. Strip ALL old resolver forms from every bash block (GSD_TOOLS=,
* GSD_SDK=, the if/elif/else/fi resolver, _GSD_SHIM_NAME=, and any
@@ -19,7 +19,7 @@
const fs = require('node:fs');
const path = require('node:path');
const WORKFLOWS_DIR = path.join(__dirname, '..', 'get-shit-done', 'workflows');
const WORKFLOWS_DIR = path.join(__dirname, '..', 'gsd-core', 'workflows');
const SNIPPET_FILE = path.join(WORKFLOWS_DIR, '_runtime-launcher.snippet.sh');
// Read canonical preamble (full content of snippet file)