feat(skill-surface): install-time profiles + runtime /gsd:surface (#3408) (#3456)

* feat(skill-deps): add requires: frontmatter to all 51 skills with cross-skill references

Mechanical migration from docs/research/data/2026-05-12-skill-audit.json.
Every skill whose body references another GSD skill now declares those
dependencies in `requires:` YAML frontmatter (flow-style array).

Notable: discuss-phase, plan-phase, and execute-phase all reference `phase`,
which confirms the latent gap in MINIMAL_SKILL_ALLOWLIST — `phase` is pulled
by the core loop but was never in the allowlist. The profile closure model
(ADR-0010 Phase 1) resolves this automatically.

Closes part of #3408.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(skill-surface-budget): add PROFILES map, resolveProfile, loadSkillsManifest, staging, marker IO

Implements the Skill Surface Budget Module core (ADR-0010, Phase 1):

- PROFILES Object.freeze map: core (6 skills), standard (~13), full ('*')
- loadSkillsManifest: parses requires: frontmatter from commands/gsd/*.md
  into a Map<stem, string[]> without external YAML dep
- resolveProfile({modes, manifest}): computes transitive closure over the
  requires: graph; composable (modes=['core','audit'] unions closures)
- stageSkillsForProfile / stageAgentsForProfile: filesystem staging with
  same exit-cleanup machinery as the legacy stageSkillsForMode
- readActiveProfile / writeActiveProfile: .gsd-profile marker round-trip
- Back-compat shims preserved: MINIMAL_SKILL_ALLOWLIST, isMinimalMode,
  shouldInstallSkill (overloaded), stageSkillsForMode — all legacy tests pass

The phase latent bug is now resolved by closure: discuss-phase, plan-phase,
and execute-phase all require phase, so any profile including any of them
automatically includes phase via transitive closure.

Tests: 22 manifest+resolve, 9 stage, 10 marker (41 new tests, all green).
Back-compat anchor: 80/80 passing.

Closes part of #3408.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(skill-surface-budget): add lint-skill-deps.cjs CI gate and fix 19 missed requires: entries

Two lint checks (scripts/lint-skill-deps.cjs):
  a) Frontmatter-body consistency: skill body references must appear in requires:
  b) Profile closure: every requires: dep of any profile skill must be in closure

Running the lint revealed 19 body references missed by the audit JSON (the
audit used static analysis; some bodies have conditional references). Fixed:
  complete-milestone: +audit-milestone, discuss-phase, plan-phase, execute-phase, new-milestone
  fast: +quick
  health: +thread
  map-codebase: +new-project, plan-phase
  new-milestone, new-project, review, ultraplan-phase: +plan-phase
  ship: +verify-work
  sketch, spike: +new-project
  verify-work: +execute-phase
  workstreams: +new-milestone, resume-work

Wired into package.json as lint:skill-deps and added to pretest.
8 fixture-based tests: all green.

Closes part of #3408.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(skill-surface-budget): wire --profile= arg, profile marker write/read in bin/install.js

- Add --profile=<name> / --profile=<n1>,<n2> arg parsing (composable).
  Mutually exclusive with --minimal / --core-only (aliases for --profile=core).
  Default (no flag): full.
- Import readActiveProfile / writeActiveProfile from install-profiles.cjs.
- After writeManifest: persist active profile to .gsd-profile marker.
- gsd update path: if no --profile flag given, read existing .gsd-profile
  marker so non-full profiles are not silently re-expanded to full (ADR-0010).
- Update --help block to document --profile= with per-tier token costs.

New test: install-minimal-backcompat.test.cjs (6 tests):
  - PROFILES.core === MINIMAL_SKILL_ALLOWLIST (contract)
  - --minimal writes .gsd-profile marker "core"
  - --profile=core, --profile=standard write correct markers
  - default install writes marker "full"

Closes part of #3408.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(changeset): add feat-3408-skill-profiles changelog fragment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(install-profiles): derive agents from skill body refs and wire into resolveProfile

Deviation 1 of ADR-0010 phase 1b: tiered profiles (core, standard) now produce
a non-empty agents Set instead of always returning empty. resolveProfile() scans
each skill body for gsd-* agent name references (via new parseCallsAgents()),
stores them in _calls_agents_<stem> manifest entries, and unions them across the
resolved skill closure. stageAgentsForProfile() already checked resolvedProfile.agents
— it now gets real data so tiered profiles install the correct subset of agents
instead of zero.

Closes #3408 (partial — Deviation 1 only)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(install): honor .gsd-profile marker on update, add resolveEffectiveProfile/mostRestrictiveProfile

Deviation 2 of ADR-0010 phase 1b: the marker written during installation is now
actually honored when re-running without explicit flags (e.g. gsd update). The
dead-end logging block is replaced by resolveEffectiveProfile(), which picks the
marker profile over 'full' when no explicit --profile= flag was given. The resolved
profile is piped through to all 13 stageSkillsForMode dispatch sites (now _stageSkills)
so updates install only the previously-chosen skill subset.

--minimal retains its back-compat behavior (strict 6-skill allowlist, no closure)
while writing 'core' to the marker. mostRestrictiveProfile() is exported for callers
that need to reconcile disagreeing markers across runtimes (smallest skill set wins).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(surface): add CLUSTERS data + state IO module

Add clusters.cjs with 10 named skill groups covering all 66 skills
(verified by surface-clusters.test.cjs). Add surface.cjs with readSurface/
writeSurface atomic IO, resolveSurface, applySurface, and listSurface.
Tests: 17 passing (11 state IO + 6 cluster integrity).

Closes #3408

* docs(adr): add ADR-0011 Skill Surface Budget Module (Phase 1 accepted, Phase 2 amendment)

Records the install-time profile staging decision (Phase 1, landed) and the
runtime /gsd:surface cluster-toggle decision (Phase 2, in flight) as an
amendment. Updates the ADR README index.

Closes #3408

* docs(install-profiles): update module docblock for Phase 2 and ADR-0011

Corrects the ADR reference from 0010 to 0011, documents the three-profile
model and back-compat aliases, adds resolveEffectiveProfile precedence rule,
and notes the companion surface.cjs Phase 2 engine.

* docs(context): add Skill Surface Budget Module canonical entry

Adds the Domain terms entry for the Skill Surface Budget Module covering
both Phase 1 (install-time profiles, .gsd-profile marker) and Phase 2
(runtime /gsd:surface cluster toggles, clusters.cjs, .gsd-surface.json),
per ADR-0011 Consequences requirement.

* feat(surface): add resolveSurface and applySurface engine + tests

Tests cover: profile → surface equivalence, cluster disable/enable,
explicitAdds transitive closure, applySurface file sync (add missing,
remove superseded, preserve non-gsd files), listSurface token cost.
16 new tests passing.

* docs(readme): document --profile= flag and /gsd:surface command

Brief user-facing mention of install profiles (core/standard/full) and the
/gsd:surface slash command in the Commands table. Points to ADR-0011 for details.

* feat(surface): add /gsd:surface slash command runbook

New skill: gsd:surface — runtime profile/cluster toggle without reinstall.
Sub-commands: list, status, profile <name>, disable/enable <cluster>, reset.
Persists state to .gsd-surface.json (independent of .gsd-profile).
Description 96 chars (≤100 limit). lint:descriptions + lint:skill-deps: 0 violations.

* feat(surface): add changeset fragment for /gsd:surface runtime toggle

* feat(surface): add surface skill stem to utility cluster

surface.md is a new skill; add it to the utility cluster so the
surface-clusters.test.cjs coverage invariant stays satisfied.

* docs(adr): fix ADR references to 0011 and record Phase 2 as shipped

ADR-0010 number was already claimed by the file-operation-engine ADR; this
ADR landed as 0011-skill-surface-budget-module.md. Update inline ADR
references in clusters.cjs, surface.cjs, install-profiles.cjs, and the
Phase 2 changeset to ADR-0011. Update the ADR Status section to record
Phase 2 artifacts as shipped on this branch rather than "in progress".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(research): port skill-surface-budget memo and audit data

ADR-0011 references docs/research/2026-05-12-skill-surface-budget.md and
docs/research/data/2026-05-12-skill-audit.json, which only existed in the
research worktree. Port both onto this branch so the ADR's References
section resolves and reviewers can read the cluster taxonomy (§3.2),
dependency topology (§3.1), and option grading (§4) that justify Phase 1
and Phase 2 decisions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(registration): register surface/clusters in INVENTORY, COMMANDS, and help.md

- surface.md: convert allowed-tools from inline YAML array to block style
  (was parsed as a single tool name "[Read, Write, Bash]" by test harness)
- docs/INVENTORY.md: add CLI module rows for clusters.cjs and surface.cjs;
  add Commands row for /gsd-surface; bump CLI Modules count 55→57, Commands 66→67
- docs/INVENTORY-MANIFEST.json: add entries for clusters.cjs, surface.cjs,
  and /gsd-surface (filename-based command key)
- docs/COMMANDS.md: add ### `/gsd-surface` heading in Configuration Commands
- get-shit-done/workflows/help.md: add /gsd:surface entry in Configuration section

Fixes registration failures introduced by Phase 2 of #3408.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(surface,docs): scrub .claude leakage and escape hypothetical slash tokens

Two PR regressions introduced earlier on this branch:

1. surface.cjs JSDoc comments contained the canonical paths
   (~/.claude/commands/gsd, ~/.claude/agents) as example values, which the
   cline-install leak regex (~\/\.claude\/(?:get-shit-done|commands|agents
   |hooks)) flagged as install-time path leaks. Reworded the docblocks to
   describe runtime-resolved paths without literal ~/.claude tokens.

2. The ported research memo proposed hypothetical Option C dispatchers
   using slash syntax (/gsd:milestone, /gsd:research). The
   docs-parity-live-registry test enforces that every slash-command token
   in docs/ resolves to a real command. Rewrote the Option C sketch
   without the slash prefix and added a clarifying note that the
   dispatchers are illustrative, not shipped.

Targeted tests now pass: tests/cline-install.test.cjs and
tests/docs-parity-live-registry.test.cjs both green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: remove raw output/source grep in lint tests

* fix: close coderabbit profile and requires issues

* test: align surface token-cost assertion wording

* fix(install): align core profile alias and defer profile marker write

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-05-13 12:45:16 -04:00
committed by GitHub
parent a60e05c714
commit d0f916728b
83 changed files with 4735 additions and 74 deletions

View File

@@ -966,6 +966,26 @@ All answers merge via `gsd-sdk query config-set`, preserving unrelated keys. API
See [CONFIGURATION.md](CONFIGURATION.md) for the full schema and defaults.
### `/gsd-surface`
Toggle which skills are surfaced — apply a profile, list, or disable a cluster without reinstall.
| Subcommand | Description |
|------------|-------------|
| `list` | Show enabled and disabled clusters and skills |
| `status` | Alias for `list` plus token cost summary |
| `profile <name>` | Write `baseProfile` and re-stage skills |
| `disable <cluster>` | Add cluster to disabled list and re-stage |
| `enable <cluster>` | Remove cluster from disabled list and re-stage |
| `reset` | Delete surface delta; return to install-time profile |
```bash
/gsd-surface list # Show current surface
/gsd-surface profile standard # Switch to standard profile
/gsd-surface disable utility # Disable the utility cluster
/gsd-surface reset # Restore install-time profile
```
---
## Brownfield Commands

View File

@@ -93,6 +93,7 @@
"/gsd-spec-phase",
"/gsd-spike",
"/gsd-stats",
"/gsd-surface",
"/gsd-thread",
"/gsd-ui-phase",
"/gsd-ui-review",
@@ -262,6 +263,7 @@
"artifacts.cjs",
"audit.cjs",
"cjs-command-router-adapter.cjs",
"clusters.cjs",
"command-aliases.generated.cjs",
"commands.cjs",
"config-schema.cjs",
@@ -303,6 +305,7 @@
"state-command-router.cjs",
"state-document.cjs",
"state.cjs",
"surface.cjs",
"template.cjs",
"uat.cjs",
"validate-command-router.cjs",

View File

@@ -54,7 +54,7 @@ Full roster at `agents/gsd-*.md`. The "Primary doc" column flags whether [`docs/
---
## Commands (66 shipped)
## Commands (67 shipped)
Full roster at `commands/gsd/*.md`. The groupings below mirror `docs/COMMANDS.md` section order; each row carries the command name, a one-line role derived from the command's frontmatter `description:`, and a link to the source file. `tests/command-count-sync.test.cjs` locks the count against the filesystem.
@@ -158,6 +158,7 @@ These six routers are descriptor-only entries that the model picks first; the bo
| `/gsd-settings` | Configure GSD workflow toggles and model profile. | [commands/gsd/settings.md](../commands/gsd/settings.md) |
| `/gsd-config` | Configure GSD settings — workflow toggles (default), advanced knobs (`--advanced`), integrations (`--integrations`), or model profile (`--profile`). | [commands/gsd/config.md](../commands/gsd/config.md) |
| `/gsd-pr-branch` | Create a clean PR branch by filtering out `.planning/` commits. | [commands/gsd/pr-branch.md](../commands/gsd/pr-branch.md) |
| `/gsd-surface` | Toggle which skills are surfaced — apply a profile, list, or disable a cluster without reinstall. | [commands/gsd/surface.md](../commands/gsd/surface.md) |
| `/gsd-update` | Update GSD to latest version; use `--sync` to sync skills across runtimes or `--reapply` to reapply local patches. | [commands/gsd/update.md](../commands/gsd/update.md) |
| `/gsd-help` | Show available GSD commands and usage guide. | [commands/gsd/help.md](../commands/gsd/help.md) |
@@ -359,7 +360,7 @@ The `gsd-planner` agent is decomposed into a core agent plus reference modules t
---
## CLI Modules (55 shipped)
## CLI Modules (57 shipped)
Full listing: `get-shit-done/bin/lib/*.cjs`.
@@ -370,6 +371,7 @@ Full listing: `get-shit-done/bin/lib/*.cjs`.
| `artifacts.cjs` | Canonical artifact registry — known `.planning/` root file names; used by `gsd-health` W019 lint |
| `audit.cjs` | Audit dispatch, audit open sessions, audit storage helpers |
| `cjs-command-router-adapter.cjs` | Shared compatibility adapter for manifest-backed CJS command-family routers |
| `clusters.cjs` | Skill cluster definitions for the runtime surface module (ADR-0011 Phase 2) |
| `command-aliases.generated.cjs` | Generated CJS alias/subcommand metadata for manifest-backed family routers |
| `commands.cjs` | Misc CLI commands (slug, timestamp, todos, scaffolding, stats) |
| `config-schema.cjs` | Single source of truth for `VALID_CONFIG_KEYS` and dynamic key patterns; imported by both the validator and the config-schema-docs parity test |
@@ -411,6 +413,7 @@ Full listing: `get-shit-done/bin/lib/*.cjs`.
| `state-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools state` |
| `state.cjs` | STATE.md parsing, updating, progression, metrics |
| `state-document.cjs` | Pure STATE.md field extraction, replacement, status normalization, and progress calculation transforms |
| `surface.cjs` | Runtime surface module — manages the runtime enable/disable surface state independently of the install-time profile marker (ADR-0011 Phase 2) |
| `template.cjs` | Template selection and filling with variable substitution |
| `uat.cjs` | UAT file parsing, verification debt tracking, audit-uat support |
| `validate-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools validate` |

View File

@@ -0,0 +1,80 @@
# Skill Surface Budget Module owns install-time profile staging and runtime surface control
- **Status:** Accepted
- **Date:** 2026-05-12
- **Decision date:** 2026-05-12
- **Implementation:** feat/3408-skills-description-dropped-due-to-size, PR <TBD>
Every installed `gsd-*` skill costs eager system-prompt tokens: runtimes (Claude Code, opencode, and others) enumerate all skill descriptions in `<available_skills>` on every turn. With 66 skills and 33 agents, GSD alone consumes roughly 60% of the default 1%-of-context skill-listing budget, causing descriptions to drop when users stack multiple plugins (#3408).
The root problem is an absence of a profile/surface seam: the installer wrote every skill unconditionally, and no runtime-side control existed for enabling or disabling a cohesive group of skills without a full reinstall.
## Decision
- Add a **Skill Surface Budget Module** under `get-shit-done/bin/lib/install-profiles.cjs` as the single owner for which skills and agents are written to runtime config directories.
- Define three named profiles: `core` (six skills covering the main loop), `standard` (core + phase management and workspace skills), and `full` (all skills — the previous default).
- Compute each profile's effective skill set as the transitive closure over the `requires:` dependency graph extracted from skill frontmatter, so partial installs never break cross-skill dependencies.
- Persist the chosen profile in a `.gsd-profile` marker file in each runtime config directory; `gsd update` reads the marker to honor the profile on re-install.
- When multiple runtimes are configured, resolve disagreement to the most-restrictive profile (smallest effective skill set).
- Allow profile composition: `--profile=core,audit` resolves to `union(closure(core), closure(audit))`.
- Preserve back-compat aliases: `--minimal` and `--core-only` map to `--profile=core`; `MINIMAL_SKILL_ALLOWLIST`, `isMinimalMode`, `shouldInstallSkill`, and `stageSkillsForMode` remain exported for existing callers.
- Add a CI gate (`scripts/lint-skill-deps.cjs`, wired into `pretest`) that verifies every skill's `requires:` entries resolve against real skill stems — prevents the profile closure from silently over-installing or breaking.
## Phase 2 — Runtime Surface Module
The Phase 2 decision, previously listed as an open question, is recorded here as an amendment to this ADR.
### Decision
- Add a `/gsd:surface` slash command with the following sub-commands:
- `list` — show all clusters and their enabled/disabled status for the active runtime
- `status` — show the active profile, effective skill count, and any dropped-description warnings
- `profile <name>` — switch the active profile and re-stage skills/agents for the current runtime
- `disable <cluster>` — mark a cluster disabled; re-stage to remove its skills from the runtime config dir
- `enable <cluster>` — mark a cluster enabled; re-stage to add its skills back
- `reset` — clear surface state and re-apply the active profile from `.gsd-profile`
- Implement the runtime surface engine in `get-shit-done/bin/lib/surface.cjs`, consuming `stageSkillsForProfile` and `stageAgentsForProfile` from the Phase 1 module without duplicating staging logic.
- Persist per-runtime surface state in `<runtimeConfigDir>/.gsd-surface.json`, independent from `.gsd-profile`. The profile marker owns install-time identity; the surface JSON owns session-scope cluster toggles.
- Source cluster taxonomy from the research memo §3.2 (2026-05-12-skill-surface-budget.md). Define clusters in `get-shit-done/bin/lib/clusters.cjs` — a separate module so the surface engine and future SDK callers can import cluster definitions without loading the full profile module.
- Cluster taxonomy: `core_loop`, `audit_review`, `milestone`, `research_ideate`, `workspace_state`, `docs`, `ui`, `ai_eval`, `ns_meta`, `utility`. Membership may overlap; every installed skill stem must appear in at least one cluster (enforced by `tests/surface-clusters.test.cjs`).
- Relationship to Anthropic platform asks: Asks D (native per-skill toggle API) and E (budget-fraction negotiation) remain filed separately. The `/gsd:surface` command is a unilateral GSD-side workaround that does not depend on those platform changes.
## Status — Phase 1 shipped
Phase 1 artifacts landed on `feat/3408-skills-description-dropped-due-to-size`:
- `get-shit-done/bin/lib/install-profiles.cjs` — `PROFILES` map, `resolveProfile`, `loadSkillsManifest`, `stageSkillsForProfile`, `stageAgentsForProfile`, `readActiveProfile`, `writeActiveProfile`, `mostRestrictiveProfile`, `resolveEffectiveProfile`
- `requires:` frontmatter added to 64 skills in `commands/gsd/*.md`
- `scripts/lint-skill-deps.cjs` — CI gate for `requires:` integrity, wired into `pretest`
- `bin/install.js` — `--profile=<name>` flag (composable); `--minimal`/`--core-only` as aliases; `.gsd-profile` marker write on install; `gsd update` re-reads marker
- Tests: `tests/install-profiles-manifest.test.cjs`, `tests/install-profiles-marker.test.cjs`, `tests/install-profiles-resolve.test.cjs`, `tests/install-profiles-stage.test.cjs`, `tests/lint-skill-deps.test.cjs`
Phase 2 shipped on the same branch:
- `commands/gsd/surface.md` — `/gsd:surface` slash command runbook (sub-commands: `list`, `status`, `profile <name>`, `disable <cluster>`, `enable <cluster>`, `reset`)
- `get-shit-done/bin/lib/surface.cjs` — runtime engine (`readSurface`, `writeSurface`, `resolveSurface`, `applySurface`, `listSurface`); reuses `stageSkillsForProfile` / `stageAgentsForProfile` from Phase 1
- `get-shit-done/bin/lib/clusters.cjs` — 10-cluster taxonomy covering all installed skill stems
- Tests: `tests/surface-state.test.cjs`, `tests/surface-clusters.test.cjs`, `tests/surface-resolve.test.cjs`, `tests/surface-apply.test.cjs`, `tests/surface-list.test.cjs`
- Persistent surface state: `<runtimeConfigDir>/.gsd-surface.json` (independent from `.gsd-profile`)
## Open questions
- Whether the `requires:` field should also be consumed by `/gsd:help` to annotate dependency chains in help output (follow-up).
- Whether telemetry (per-profile install counts, cluster-disable events) should be added to the surface engine or deferred.
- Whether Anthropic platform asks D and E (native skill toggle API, budget-fraction negotiation) should block any future work in this module.
## Consequences
- Users with constrained context budgets can install `--profile=core` and expand incrementally via `/gsd:surface enable <cluster>` without a full reinstall.
- The `requires:` closure ensures partial installs never silently break skill cross-references.
- Future skills must declare `requires:` dependencies to participate in profile resolution; the lint gate enforces this at CI time.
- `CONTEXT.md` gains a canonical **Skill Surface Budget Module** entry; future architecture reviews should treat out-of-seam skill staging as drift.
- Cluster definitions in `clusters.cjs` are the authoritative taxonomy for runtime surface control; additions must be reflected there and in tests.
## References
- Feature issue: `#3408`
- Research memo: `docs/research/2026-05-12-skill-surface-budget.md` (§3.2 cluster taxonomy)
- See `0008-installer-migration-module.md`
- See `0009-shell-command-projection-module.md`
- See `0010-file-operation-engine-module.md`

View File

@@ -18,6 +18,7 @@ Each ADR documents one architectural decision: what was decided, why, and what c
| [0008-installer-migration-module.md](0008-installer-migration-module.md) | Installer Migration Module owns install-time upgrade safety | Accepted |
| [0009-shell-command-projection-module.md](0009-shell-command-projection-module.md) | Shell Command Projection Module owns runtime-aware OS command rendering | Accepted |
| [0010-file-operation-engine-module.md](0010-file-operation-engine-module.md) | File Operation Engine Module owns safe runtime/config file mutations | Proposed |
| [0011-skill-surface-budget-module.md](0011-skill-surface-budget-module.md) | Skill Surface Budget Module owns install-time profile staging and runtime surface control | Accepted |
## Seam map
@@ -33,3 +34,8 @@ projection of installer-owned command text and projection IR.
ADR 0010 documents the File Operation Engine Module seam for converging
installer/migration/planning file mutation safety policy, and its relationship
to ADR 0009 hook-command ownership policy.
ADR 0011 documents the Skill Surface Budget Module for install-time skill/agent
profile staging (`--profile=<name>`, `.gsd-profile` marker, `requires:` closure)
and the Phase 2 runtime `/gsd:surface` command for cluster-level enable/disable
without reinstall.

View File

@@ -0,0 +1,269 @@
# Skill surface budget — research memo
**Date:** 2026-05-12
**Author:** triage analysis for issue [#3408](https://github.com/gsd-build/get-shit-done/issues/3408)
**Status:** Research input for ADR-0010
**Reading time:** ~15 min
---
## 1. Problem statement
Claude Code and other runtimes that surface skills enumerate every installed skill's `name + description` into the system prompt on every turn, inside an `<available_skills>` block. This block is capped by `skillListingBudgetFraction` — default **1%** of the model's context window, ~**2,000 tokens** at 200k. When the combined descriptions exceed the cap, the harness silently truncates the tail. The reporter of #3408 has 135 installed skills across multiple plugins and observed dropped skills.
GSD's audited footprint:
| Metric | Value |
|---|---|
| Installed skills | 66 |
| Installed sub-agents | 33 (not listed in `<available_skills>` — invoked via `Task`) |
| Total skill description chars | 4,787 |
| Estimated description tokens (÷4) | ~1,196 |
| Mean description length | 72.5 chars |
| Description ceiling (enforced by `scripts/lint-descriptions.cjs`) | 100 chars |
| GSD share of a 200k 1% budget | ~60% |
GSD on its own consumes roughly 60% of the default skill-listing budget. When the user stacks any other plugin of comparable size (the reporter's `/doctor` shows several), the budget is breached and skills get dropped. This is not a GSD-only problem, but **GSD is the single biggest contributor in the typical install**, so we are the natural place for ecosystem-wide remediation to start.
## 2. What's already in place
GSD has done one consolidation pass and shipped one install-time lever:
- **`--minimal` / `--core-only` install flag** (`bin/install.js:123`, `get-shit-done/bin/lib/install-profiles.cjs`). Stages a filtered copy of `commands/gsd/` into a temp dir before each runtime-specific copy step. Reduces ~12k tokens of cold-start overhead to ~700.
- **`MINIMAL_SKILL_ALLOWLIST`** — 6 skills: `new-project`, `discuss-phase`, `plan-phase`, `execute-phase`, `help`, `update`. Zero sub-agents in minimal.
- **Hard 100-char description budget**, enforced in CI by `scripts/lint-descriptions.cjs` and `npm run lint:descriptions`.
- **`gsd update` (without `--minimal`)** as the documented upgrade path from minimal → full.
The 100-char cap means **shrinking descriptions further is not a viable lever** — average is already 72.5 chars and the rare 99-char outliers exist because they earn their length (e.g. `gsd:progress`, `gsd:inbox`). Any future budget relief has to come from **emitting fewer skills**, not shorter ones.
## 3. Audit findings
### 3.1 Dependency topology (66 skills)
Hot nodes (counted by other-skill body references):
| Rank | Skill | Callers | Role |
|---|---|---|---|
| 1 | `phase` | 38 | Dispatcher to all phase-typed workflows |
| 2 | `review` | 11 | Review-output convergence |
| 3 | `config` | 7 | Project config display/edit |
| 4 | `progress` | 5 | Active-phase progress tracker |
| 5 | `update` | 5 | Upgrade path |
| 6 | `discuss-phase` | 2 | Main-loop step 1 |
| 6 | `execute-phase` | 2 | Main-loop step 3 |
| 6 | `new-project` | 2 | Bootstrap |
| 6 | `plan-phase` | 2 | Main-loop step 2 |
`phase` and `review` are the two skills whose absence would silently break dozens of others. **`phase` is referenced by 38 other skills but is not in the current minimal allowlist** — a latent gap worth raising. Confirm with a `--minimal` install + a `/gsd:audit-fix` invocation whether it still works.
### 3.2 Functional clusters
Greedy clustering by name + prose intent:
| Cluster | Skills | Desc chars | Desc tokens |
|---|---|---|---|
| `core_loop` | 6 | 346 | ~87 |
| `phase_variants` (includes `core_loop`) | 10 | 734 | ~184 |
| `audit_review` | 11 | 772 | ~193 |
| `milestone` | 4 | 292 | ~73 |
| `research_ideate` (`sketch`, `spike`, `forensics`, `explore`, `graphify`, `ns-ideate`) | 6 | 482 | ~121 |
| `workspace_state` (`pause`, `resume`, `workspace`, `workstreams`, `thread`, `capture`, `inbox`) | 7 | 496 | ~124 |
| `docs` | 2 | 160 | ~40 |
| `ui` | 2 | 122 | ~31 |
| `ai_eval` | 2 | 179 | ~45 |
| `ns_meta` | 6 | 315 | ~79 |
| `utility` (incl. `health`, `stats`, `settings`, `cleanup`, `pr-branch`, `ship`, `undo`, `fast`, `quick`, …) | 23 | 1,714 | ~429 |
The **utility** bucket is the heaviest single cluster (36% of description tokens) and is also the most heterogeneous — half of these likely run only once per project, not once per session. They're the prime candidates for an "advanced" or "opt-in" tier.
### 3.3 Consolidation ceiling
A second consolidation pass (collapsing all 10 `*-phase` skills into a single dispatcher) would save ~150 tokens at most, at the cost of making the user-facing slash commands less discoverable in the UI and complicating argument parsing. **Pure consolidation has diminishing returns** below the current 66-skill count; the next big saves come from changing *what gets surfaced* rather than *how it's written*.
## 4. Options
Each option is graded against five dimensions:
- **User UX**: how surprising / how much new mental model
- **Implementation cost**: relative dev effort
- **Dependency safety**: risk of breaking cross-skill calls
- **Token savings**: vs. current ~1,196 desc tokens
- **Anthropic dependency**: whether platform changes are required
### Option A — Expand install-time profiles (named feature flags)
Layer named profiles on top of the existing minimal/full binary:
```
gsd install --profile=core # current --minimal (6 skills, 0 agents)
gsd install --profile=standard # +phase, +review, +config, +progress (~14 skills)
gsd install --profile=full # current default (66 skills, 33 agents)
gsd install --profile=core,audit,ui # composable feature tags
```
| Dimension | Assessment |
|---|---|
| User UX | Familiar pattern (Cargo features, Helm `--set`, Ansible tags). Picker prompt on interactive install. |
| Implementation cost | **Low.** `install-profiles.cjs` already does the staging. Extend `MINIMAL_SKILL_ALLOWLIST` into a map of profile → set, add a `--profile` arg parser, add interactive `AskUserQuestion`. |
| Dependency safety | Need a manifest declaring each skill's required-skills set, so a profile can't ship an orphan caller. Add as CI lint. |
| Token savings | High — `standard` cuts ~70% of descriptions; named clusters give users granular control. |
| Anthropic dependency | None. |
**Pros:** ships unilaterally, leverages existing seam, low blast radius.
**Cons:** install-time only — users on a "full" install can't shrink without reinstall.
### Option B — Runtime enable/disable command
A `/gsd:surface` (or `gsd surface` CLI) command that toggles which skills are visible to the runtime without touching installed files:
```
/gsd:surface list # show enabled/disabled
/gsd:surface disable ui audit # hide a cluster
/gsd:surface profile standard # apply a named profile
```
Implementation: write enable/disable state to `~/.claude/skills/<name>/SKILL.md.disabled` (rename) or maintain a `gsd-surface.json` manifest the installer reads on every `update`.
| Dimension | Assessment |
|---|---|
| User UX | Discoverable through `/gsd:help`. Lower commit than reinstall. Mirrors VS Code's enable/disable extension UX. |
| Implementation cost | **Medium.** Need persistent state separate from install files, plus a re-apply loop on `gsd update`. |
| Dependency safety | Same manifest requirement as Option A — disabling `phase` should warn that 38 skills depend on it. |
| Token savings | High — user-driven; can match Option A's savings. |
| Anthropic dependency | None for the rename approach. Cleaner if Anthropic supports a `SKILL.disabled` convention natively. |
**Pros:** in-session adjustable, no reinstall friction.
**Cons:** state lives outside the installer's idempotent model, so `gsd update` migrations get more complex.
### Option C — Further skill consolidation
Collapse semantically related skills into a single dispatcher with sub-modes:
```text
gsd-phase → keeps existing
gsd-milestone {new|complete|summary|audit} # was 4 skills (hypothetical)
gsd-research {sketch|spike|forensics|explore} # was 4 skills (hypothetical)
gsd-workspace {pause|resume|capture|inbox|thread} # was 5 skills (hypothetical)
```
(The hypothetical dispatchers above are written without the slash prefix to signal they are not shipped commands — Option C is a sketch, not a recommendation.)
| Dimension | Assessment |
|---|---|
| User UX | Breaking change for muscle-memorized slash commands; needs aliases for ≥1 release cycle. |
| Implementation cost | **Medium-high.** Argument-parsing inside each dispatcher; migration of cross-references in 30+ skill bodies; aliases; CHANGELOG entries. |
| Dependency safety | High — every cross-reference in existing skill bodies needs rewriting. The audit graph (3.1) is the migration spec. |
| Token savings | Moderate — ~200-300 tokens by collapsing the 13-15 named skills above into 3 dispatchers. |
| Anthropic dependency | None. |
**Pros:** also improves IA — the slash-command surface becomes more discoverable.
**Cons:** breaks user habits, doesn't compose with A/B (you still want profiles after consolidating).
### Option D — Lazy / on-demand descriptions (Anthropic ask)
Skills ship a 1-line *teaser* in the system prompt and the full description loads only when the model expresses interest (analogous to `ToolSearch` for deferred tools). Cuts per-skill listing cost to ~10 chars.
| Dimension | Assessment |
|---|---|
| User UX | Invisible to users. |
| Implementation cost | **Low for GSD** — add a `teaser:` frontmatter field. **High for Anthropic** — harness changes. |
| Dependency safety | N/A (purely about listing). |
| Token savings | ~85% of all skill-listing budget across the ecosystem. |
| Anthropic dependency | **Yes — platform feature.** |
This is the architecturally correct long-term answer. GSD can't ship it alone.
### Option E — Per-plugin budget allocation (Anthropic ask)
Instead of one shared `skillListingBudgetFraction`, give each plugin a dedicated quota (e.g. proportional to declared `skills.count` × ceiling). Eliminates one greedy plugin starving others.
| Dimension | Assessment |
|---|---|
| User UX | Invisible. |
| Implementation cost | **Low for GSD** — declare quota in `package.json` / plugin manifest. **Medium for Anthropic** — quota arithmetic and tie-breaking in the harness. |
| Token savings | Doesn't reduce total, but eliminates the silent-drop failure mode. |
| Anthropic dependency | **Yes.** |
### Option F — Sub-plugins / split distribution
Publish GSD as multiple npm packages: `get-shit-done-cc-core`, `get-shit-done-cc-milestones`, `get-shit-done-cc-research`, etc. Users install only what they need.
| Dimension | Assessment |
|---|---|
| User UX | Reasonable for advanced users; confusing for first-time installers. Needs a meta-package (`get-shit-done-cc`) that depends on the slim ones — analogous to VS Code extension packs. |
| Implementation cost | **High.** Multi-package build pipeline, version sync across packages, changelog routing, install-script forking. |
| Dependency safety | npm semver carries the contract; cross-package refs become real `require()` calls. |
| Token savings | Same as Option A in practice — token savings come from choosing not to install, not from the package boundary. |
| Anthropic dependency | None. |
**Pros:** clean separation, follows npm-ecosystem norms.
**Cons:** very high lift for the same token savings Option A delivers.
## 5. Recommendation
**Adopt Option A (named install profiles) as ADR-0010, with Option B (runtime surface toggle) as a Phase-2 amendment**. File Options D and E to Anthropic as platform asks.
Why this ordering:
1. **A reuses an existing seam.** `install-profiles.cjs` is already the staging point; this is the lowest-risk way to ship meaningful relief in the next release.
2. **A is composable.** Naming clusters as profiles is a forcing function for the dependency manifest, which we want anyway for the lint described in 3.1.
3. **B follows A naturally.** Once profiles exist, the `/gsd:surface` command is "apply a profile to a live install plus persist deltas." Without A, B has no profiles to apply.
4. **C is independent and orthogonal.** It can happen in parallel as IA cleanup; it should not block A.
5. **D and E are platform-level.** GSD ships A regardless; D/E are documented as cooperative asks so Anthropic sees them in context.
## 6. Anthropic platform asks
Drafted for filing at <https://docs.claude.com/feedback> or similar channel; copy unchanged into the issue or feedback form.
### Ask 1 — Native lazy skill descriptions
> Skills currently emit `name + description` into every system prompt. For large plugin ecosystems (135+ skills on power-user installs) this overruns `skillListingBudgetFraction` and silently drops skills. Proposal: add a frontmatter `teaser:` field (≤40 chars) that ships in the listing, with the full `description:` loaded only when the model requests it (analogous to ToolSearch for deferred tools). Backwards-compatible: skills without `teaser:` keep current behavior.
### Ask 2 — Per-plugin budget allocation
> A single `skillListingBudgetFraction` shared across all installed plugins causes silent truncation when one plugin's skill set is large. Proposal: each plugin declares a soft quota in its manifest; the harness arbitrates fairness when total demand exceeds budget (e.g. proportional shrink, with a documented order — most-recently-installed last to be cut). Surface drops in `/doctor` output today; do not drop silently.
### Ask 3 — Dependency-aware skill listing
> Skills can call other skills (in GSD, the `phase` skill is referenced by 38 others). When the harness drops a skill from the listing, it has no way to know whether anything else relies on it. Proposal: optional frontmatter `requires: [other-skill]` so the harness keeps the closure of dependencies in the listing, or warns the user at install time that a dropped skill is reachable from a kept one.
### Ask 4 — Disable/enable without uninstall
> Today the only way to remove a skill from the listing is to delete its `SKILL.md`. Proposal: a `.disabled` suffix (e.g. `SKILL.md.disabled`) or a per-skill `enabled: false` frontmatter is treated as "not surfaced" by the harness. This lets plugins ship surface-toggle UIs (like our proposed `/gsd:surface disable`) without touching install state.
## 7. Implementation sketch (for the ADR)
Phase 1 — profiles (ships with ADR-0010):
1. In `get-shit-done/bin/lib/install-profiles.cjs`, replace the single `MINIMAL_SKILL_ALLOWLIST` constant with a `PROFILES` map. Each profile is the *transitive closure* over a base set, so `standard` includes `core` automatically.
2. Add a `requires:` frontmatter field to every skill that calls another skill in its body. Add a lint check in `scripts/lint-descriptions.cjs` (or a sibling `lint-skill-deps.cjs`) that fails CI if a skill body references another skill that isn't in its `requires` list, and that fails if any profile would ship a skill whose `requires` aren't satisfied.
3. Extend the `bin/install.js` argument parser: `--profile=<name>` (mutually exclusive with `--minimal`), `--profile=core,audit` for composition. Keep `--minimal` as an alias for `--profile=core`.
4. Interactive install: if no `--profile` is given and no runtime/location is forced, present an `AskUserQuestion`-style picker. (Cowork analog already in the install flow.)
5. `gsd update` re-applies the recorded profile from a small marker file (`~/.claude/skills/.gsd-profile`).
Phase 2 — runtime surface command (follow-up ADR or amendment):
1. `/gsd:surface` command writes to the profile marker and re-runs the staging step for the active runtime.
2. Once Anthropic ships Ask 4, switch from file-deletion to `.disabled`-suffix toggling.
## 8. Follow-ups outside this scope
- **Stale comment in `install-profiles.cjs`.** The module-level header cites "86 skills + 33 agents" producing ~12k tokens. The audited count is 66 + 33 — a previous consolidation pass already happened. Update the comment in a parallel cleanup commit when ADR-0010 lands.
- **Audit JSON refresh.** `docs/research/data/2026-05-12-skill-audit.json` is a one-shot snapshot. If we want it to stay current, wire the extraction script into `scripts/` and run it on `lint:skill-deps`. Not a blocker.
## 9. Risks and unknowns
- **The `phase` dispatcher gap in the existing minimal allowlist.** Confirm whether a fresh `--minimal` install + the documented main loop actually works end-to-end. If `discuss-phase`/`plan-phase`/`execute-phase` silently fall back to `/gsd:phase`, the minimal allowlist is currently broken. Track as a separate bug if confirmed.
- **Profile naming bikeshed.** `core` / `standard` / `full` vs. `minimal` / `recommended` / `everything` vs. functional names (`planning`, `audit`, `research`). Settle in the ADR's Open Questions.
- **Discoverability of disabled skills.** If `gsd:audit-fix` isn't surfaced, a user asking "audit my project" won't get it suggested. `/gsd:help` should list installed-but-not-surfaced skills with a one-line upgrade hint.
- **Telemetry blind spot.** GSD doesn't currently know which skills users invoke, so "drop the long tail" is theoretical. Survey or self-reporting may be needed before drawing the `standard` profile line.
## 10. References
- Issue: [#3408](https://github.com/gsd-build/get-shit-done/issues/3408)
- Existing seam: `get-shit-done/bin/lib/install-profiles.cjs`
- Description lint: `scripts/lint-descriptions.cjs`
- Install dispatcher: `bin/install.js:123` (mode parsing), `bin/install.js:8167-8207` (minimal staging)
- Audit data: [`docs/research/data/2026-05-12-skill-audit.json`](data/2026-05-12-skill-audit.json) (per-skill dep graph, description sizes, and cluster mapping — reproducible from `commands/gsd/` and `agents/`)
- Prior ADRs: 0008 (Installer Migration Module) and 0009 (Shell Command Projection Module) — both touch the same install pipeline this proposal extends.
- Ecosystem precedents: Cargo `[features]`, npm `optionalDependencies`, VS Code extension packs, Homebrew taps, Helm chart values, Ansible role tags, Linux kernel `make menuconfig` tristate, systemd target activation.

File diff suppressed because it is too large Load Diff