Files
msd-core/docs/adr/0010-skill-surface-budget-module.md
Tom Boucher 463cffd894 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>
2026-06-02 18:35:29 -04:00

12 KiB

Skill Surface Budget Module owns install-time skill listing curation

  • Status: Proposed
  • Date: 2026-05-12

We propose extending the existing install profile seam (gsd-core/bin/lib/install-profiles.cjs) into a Skill Surface Budget Module that owns which subset of GSD's 66 skills is written to the runtime config dirs, and that owns the per-skill requires: dependency manifest used to keep that subset closed under cross-skill references. GSD currently ships a binary --minimal / full toggle; runtimes that enumerate skills (Claude Code, OpenCode, etc.) cap the <available_skills> system-prompt block at skillListingBudgetFraction of the context window (default 1% = ~2k tokens at 200k), and GSD alone consumes ~60% of that cap (#3408). Further description shrinkage is unavailable — scripts/lint-descriptions.cjs already enforces a hard 100-char ceiling and the mean is 72.5 chars. The remaining lever is surfacing fewer skills, which requires a typed profile model plus a dependency manifest, not more ad-hoc allowlists.

Decision

  • Add a Skill Surface Budget Module by extending gsd-core/bin/lib/install-profiles.cjs as the single owner for which commands/gsd/*.md and agents/gsd-*.md files are staged into the per-runtime copy pipeline.
  • Replace the single MINIMAL_SKILL_ALLOWLIST constant with a typed PROFILES map keyed by profile name. Each profile is a base set of skills; the module computes the transitive closure over each skill's declared requires: set before staging.
  • Add a requires: frontmatter field to every skill whose body references another GSD skill. The dependency graph in the research memo (docs/research/2026-05-12-skill-surface-budget.md §3.1) is the migration spec for this pass.
  • Extend bin/install.js argument parsing to accept --profile=<name> and --profile=<name1>,<name2> (composable). Preserve --minimal / --core-only as aliases for --profile=core. Default install (no flag) remains full for back-compat.
  • Persist the active profile to ~/.claude/skills/.gsd-profile (and runtime-equivalent locations) so gsd update re-applies the same profile instead of expanding silently to full.
  • Add scripts/lint-skill-deps.cjs and wire it into the existing npm run lint:descriptions pretest gate. The lint fails if:
    • a skill body references another skill not in its requires: set, or
    • any profile would ship a skill whose requires: closure is not satisfied.
  • Keep the interactive install picker behind the same AskUserQuestion-style flow already used for runtime/location selection. Non-interactive installs (CI, npx --yes) fall back to --profile=full unless overridden.

Initial Scope

First migration slice should land the profile model and one new tier above core:

  1. Profile map (typed): core (current minimal, 7 skills including phase), standard (~13 skills covering the audit + main-loop + utility floor), full (current default, 66 skills).
  2. requires: frontmatter added to the hot nodes of the dependency graph first: phase (38 callers), review (11), config (7), progress (5), update (5). These are the skills whose absence silently breaks others, so they need explicit required_by audit before any profile narrows them out.
  3. Confirm-and-lock the latent bug fix surfaced by the audit: phase is referenced by 38 skills and now belongs in MINIMAL_SKILL_ALLOWLIST / PROFILES.core. Keep explicit coverage in minimal/core tests so this cannot regress.
  4. CLI surface: --profile=, comma-composed profiles, --profile=help listing each profile's contents and token cost.
  5. Profile marker persistence + gsd update re-application.

It should not in the first pass:

  • Build a runtime enable/disable surface (/gsd:surface). Track as a follow-up ADR (see "Open questions").
  • Split GSD into multiple npm packages. The packaging-level alternative was considered and rejected — see research memo §4 Option F.
  • Consolidate further skills (e.g. collapsing *-phase into a dispatcher). Track separately as IA cleanup; orthogonal to surface curation.

Migration Inventory

gsd-core/bin/lib/install-profiles.cjs

  • Replace MINIMAL_SKILL_ALLOWLIST Object.freeze constant with PROFILES Object.freeze map of profile-name → base skill set.
  • Replace isMinimalMode(mode) with resolveProfile(mode) returning a typed {name, skills: Set, agents: Set} after transitive-closure computation.
  • Replace shouldInstallSkill(name, mode) with shouldInstallSkill(name, resolvedProfile).
  • Replace stageSkillsForMode(srcDir, mode) with stageSkillsForProfile(srcDir, resolvedProfile). Add a sibling stageAgentsForProfile since this module now owns agent staging too (current --minimal skips agents wholesale; tiered profiles need finer control).
  • Keep the existing exit-cleanup machinery (STAGED_DIRS, ensureExitCleanup) unchanged — the bug surface it covers is the same.

bin/install.js

These call sites should migrate behind the Skill Surface Budget Module:

  • --minimal / --core-only flag parsing — bin/install.js:123-124
  • _effectiveInstallMode plumbing + isMinimalMode() checks — bin/install.js:7634-8465 (passes through to per-runtime copy fns)
  • minimal-agent skip block — bin/install.js:8167-8207 (becomes "skip agents not in profile")
  • runtime-specific copy entry points that consume stageSkillsForMode — 13 sites per the existing comment in install-profiles.cjs
  • usage help block — bin/install.js:508 (add --profile= documentation)

Frontmatter changes

  • Add requires: field to every skill in commands/gsd/*.md whose body references another GSD skill. Audit data lists the full set (docs/research/2026-05-12-skill-surface-budget.md §3.1). Estimate: 25-30 files touched in Phase 1.
  • Field is optional. Absence = "no GSD-skill dependencies." lint-skill-deps.cjs enforces consistency, not presence.

New: scripts/lint-skill-deps.cjs

  • Walks commands/gsd/*.md, parses requires:, walks the body for gsd:<name> or \b<stem>\b references to other skills (same matching rules documented in docs/research/2026-05-12-skill-surface-budget.md §3.1).
  • Fails CI if requires: set ≠ actual references (modulo ignore-list for prose mentions that aren't actual dispatches).
  • Walks PROFILES from install-profiles.cjs, fails if any profile's transitive closure references a skill not in the profile.
  • Wires into npm run lint:descriptions (or as a sibling lint:skill-deps) and pretest.

Profile marker

  • New ~/.claude/skills/.gsd-profile (and per-runtime equivalents under .codex/, .cursor/, etc. as enumerated in install.js) containing the active profile name.
  • Installer Migration Module (ADR-0008) gains a one-shot migration: if marker absent and skills dir matches core exactly, write core; otherwise write full. Migrations are idempotent per existing module contract.

Tests expected to move with the seam

  • tests/install-profiles-*.test.cjs (any existing) — extend to assert profile resolution, transitive closure, and --profile=core,standard composition.
  • New tests/skill-surface-budget-*.test.cjs covering:
    • profile closure: a profile that lists discuss-phase must transitively include phase if discuss-phase requires it
    • lint failures: a skill body that references an un-required skill makes lint:skill-deps fail
    • marker persistence: gsd install --profile=standard followed by gsd update preserves standard
    • minimal back-compat: --minimal resolves to --profile=core and emits the same file set as today (modulo the phase-inclusion bug fix)

Interface sketch

The module should accept typed profile intent and return a typed resolved profile:

// install-profiles.cjs (extended)
resolveProfile({
  modes: ['core' | 'standard' | 'full'],
  skillsManifest: ManifestMap,   // parsed `requires:` graph
})
// → { name: 'standard', skills: Set<string>, agents: Set<string> }

Profile composition: --profile=core,standard resolves to union(closure(core), closure(standard)). --profile=full is the identity profile (every skill).

stageSkillsForProfile(srcDir, resolvedProfile)  // returns staged dir path
stageAgentsForProfile(srcAgentsDir, resolvedProfile)  // new

Profile marker IO is typed too, not stringly:

readActiveProfile(runtimeConfigDir) // → 'core' | 'standard' | 'full' | null
writeActiveProfile(runtimeConfigDir, profileName)

Per-skill frontmatter contract:

---
name: gsd:plan-phase
description: ...
requires: [phase, discuss-phase]   # GSD skills only; not Claude Code primitives
---

requires: lists GSD skills (file stems). It does not include Claude Code built-ins (Read, Bash, etc.) — those continue to live in allowed-tools: per existing convention.

Consequences

  • The skill-set written by the installer becomes a typed first-class artifact, not a side effect of file copies + an allowlist constant. ADR-0008 (Installer Migration Module) gains a clean handle for safe profile migrations on upgrade.
  • gsd update stops silently re-expanding a --minimal install to full — a current foot-gun documented inline in install-profiles.cjs (its module-level comment recommends gsd update without --minimal to "expand to the full surface"; that path remains available, but the default gsd update now respects the recorded profile).
  • The requires: manifest creates a new authoring obligation (~30 files in Phase 1), enforced by CI. Skill authors who add a /gsd:phase reference in a new skill body have to update requires:. The lint script keeps drift low-cost.
  • The phase-in-minimal latent gap (research memo §3.1) gets resolved as a side effect of adopting closure-based profile resolution — phase is auto-included whenever any minimal-loop skill requires: it.
  • First-time install UX gains a profile picker. The default remains full for non-interactive (npx --yes) installs, so back-compat for CI scripts is preserved.
  • The module becomes the canonical place to land future Anthropic platform features (lazy descriptions, per-plugin budgets, .disabled toggles — see Open Questions). It does not, in this ADR, use those features.
  • If accepted, CONTEXT.md should gain a canonical Skill Surface Budget Module entry alongside the existing seam entries, and future architecture reviews should treat ad-hoc commands/gsd/ filtering outside this seam as drift.

Open questions

  • Whether the Phase-2 runtime /gsd:surface command (research memo §4 Option B) should be its own ADR or an amendment to this one. Leaning separate ADR because it introduces persistent runtime state outside the install pipeline.
  • Profile naming bikeshed. core / standard / full is the working proposal. Alternatives surveyed: minimal / recommended / everything, functional names (planning, audit, research). Settle in the implementation PR after a contributor poll.
  • Whether the requires: field should also be consumed by /gsd:help to render a "skills you have installed and what depends on what" graph. Likely yes, but out of scope for this ADR.
  • Whether to keep phase explicitly listed in core forever vs relying purely on closure semantics. Current recommendation: keep explicit listing because minimal mode has a back-compat allowlist path.
  • Whether telemetry (opt-in) is worth proposing to inform where the standard profile line goes. Without it, the cut points are author-intuition. Track separately; not a blocker.
  • Whether the Anthropic platform asks (research memo §6 — lazy descriptions, per-plugin budgets, dependency-aware listing, .disabled toggles) should be filed before or after this ADR ships. Recommendation: file as a feedback bundle when ADR is accepted, so we ship Phase 1 unilaterally and platform improvements compose on top.

References

  • Feature issue: #3408
  • Research input: docs/research/2026-05-12-skill-surface-budget.md
  • Existing seam being extended: gsd-core/bin/lib/install-profiles.cjs
  • Description budget enforcement: scripts/lint-descriptions.cjs
  • Installer dispatch site: bin/install.js:123-124, :8167-8207
  • See 0008-installer-migration-module.md (the migration that records the profile marker lives here)
  • See 0005-sdk-architecture-seam-map.md (the seam map this module joins)