Quick Reference
Features Covered
| Feature | Module | Version |
|---|---|---|
| 6-Stage Pipeline | src/application/skill-pipeline.ts | v1.8 |
| Token Budgeting | src/validation/budget-validation.ts | v1.8 |
| Model Profiles | src/application/budget-profiles.ts | v1.11 |
| Skill Inheritance | src/composition/skill-resolver.ts | v1.11 |
Prerequisites
- GSD Skill Creator installed and initialized
- At least one skill created in
.claude/commands/ - Understanding of skill format and metadata
- Familiarity with YAML frontmatter in SKILL.md files
- Basic understanding of token/character budgets in Claude Code
Pipeline Stages
Input
intent / file / context
|
1. Score
(find matching skills via
SkillIndex + TF-IDF)
|
2. Resolve
(merge inherited content
via SkillResolver)
|
3. ModelFilter
(remove model-incompatible
skills)
|
4. CacheOrder
(prioritize recently-used
skills)
|
5. Budget
(enforce character and
token limits)
|
6. Load
(read from disk or cache,
return content)
|
Output
loaded[] / skipped[]
Configuration Reference
Character budget (Claude Code layer):
- Per-skill limit: 15,000 chars
- Cumulative limit: 15,500 chars
- Env override:
SLASH_COMMAND_TOOL_CHAR_BUDGET
Budget severity thresholds:
- ok: <60% usage
- info: 60-79% usage
- warning: 80-99% usage
- error: ≥100% usage
Application defaults (ApplicationConfig):
- Context window: 200,000 tokens
- Budget percent: 3% (6,000 tokens)
- Relevance threshold: 0.1
- Max skills per session: 5
Agent budget profiles:
| Agent | Standard | Ceiling |
|---|---|---|
| gsd-executor | 6% | 10% |
| gsd-planner | 5% | 8% |
| gsd-verifier | 5% | 8% |
| gsd-debugger | 6% | 10% |
| gsd-researcher | 5% | 8% |
Priority tiers:
critical: Always loads (up to hard ceiling)standard: Loads within standard budget (default tier)optional: Loads only if budget remains
Model capability tiers:
- Opus = 3 (highest capability)
- Sonnet = 2 (balanced)
- Haiku = 1 (fastest, lowest capability)
Key TypeScript Interfaces
PipelineContext— Data contract between stagesPipelineStage— Interface for custom stagesScoredSkill— Skill with relevance scoreConflictResult— Conflict detection resultSkillResolution— Merged inheritance resultBudgetProfile— Per-agent budget configBudgetCheckResult— Single skill budget resultCumulativeBudgetResult— All-skills budget resultSkippedSkill— Why a skill was not loadedBudgetWarning— Threshold warning objectTokenCountResult— Count with source trackingModelGuidance— Model compatibility metadata
Related Guides
Overview
When Claude Code starts a session and you begin working, skill-creator needs to decide which of your skills are relevant right now. Not all skills should load every time — loading everything would consume too much of the context window, pushing out space for your actual work. The activation pipeline solves this problem with a 6-stage filtering and loading process.
The pipeline takes three inputs — your intent (what you said), the file you are working on, and the current context — and produces one output: a list of skill contents to inject into the session. Between input and output, each stage progressively narrows the set of candidates, resolves inheritance relationships, filters by model compatibility, prioritizes recently-used skills, enforces budget constraints, and finally loads the content from disk.
The design is deliberately sequential and deterministic. Given the same inputs and skill inventory, the pipeline always produces the same output. There is no randomness, no probabilistic ranking at the pipeline level — the TF-IDF scoring in Stage 1 is deterministic for a given corpus. This makes the system predictable, testable, and debuggable. You can trace exactly why a skill was loaded or skipped by examining each stage's input and output.
Key Concepts
Think of it like packing a backpack for a day hike. You have a closet full of gear (all your skills), but your backpack (the context window) has limited space. The pipeline is the packing process:
- Score — Look at the weather forecast and trail map. Which gear is relevant to today's hike? Your rain jacket scores high if it is raining, but your snowshoes score zero if it is summer. This stage matches your current task to the right skills.
- Resolve — Some gear comes in sets. Your "winter hiking" kit includes a base layer that came from the "all-season" kit. The resolve stage unpacks these sets so you get the complete gear, not just the top-level item.
- ModelFilter — Some gear only works with specific boots. If you are wearing trail runners, the crampons get removed. This stage removes skills that are not compatible with the current AI model.
- CacheOrder — You used your headlamp yesterday and it worked great. Gear you have used recently goes toward the top of the packing list, because recently useful gear tends to stay useful.
- Budget — Your backpack only holds 30 liters. Pack the essential items first (critical tier), then the standard items, then optional items only if space remains. If the backpack is full, the extra items stay home with a note saying why.
- Load — Actually put the items in the backpack. Read the skill files from disk and hand them to Claude Code.
The result: Claude Code starts with exactly the right skills loaded — not too many (wasting context), not too few (missing relevant knowledge).
Teaching tip: Have students install 3-4 skills and then run npx skill-creator status to see the budget breakdown. Ask them to predict which skills would be dropped if a large new skill were added. This illustrates resource management and prioritization in a tangible way.
How It Works
The Pipeline Architecture
The pipeline is implemented as the SkillPipeline class — a sequential stage runner that processes a PipelineContext through an ordered list of stages. Each stage implements the PipelineStage interface:
interface PipelineStage {
readonly name: string;
process(context: PipelineContext): Promise<PipelineContext>;
}
The context is a data object with a fixed shape that carries all information between stages. It has clearly defined responsibilities:
- Inputs (set by the caller, read-only to stages):
intent,file,context,modelProfile - Intermediate results (stages read and write):
matches,scoredSkills,resolvedSkills,conflicts - Outputs (final results):
loaded,skipped,budgetSkipped,budgetWarnings,contentCache - Control:
earlyExitflag that stages can check to skip their logic - Session:
getReport()function providing access to budget tracking data
The pipeline runner is intentionally minimal. It iterates stages in order, passing the context through each one:
async process(context: PipelineContext): Promise<PipelineContext> {
let ctx = context;
for (const stage of this.stages) {
ctx = await stage.process(ctx);
}
return ctx;
}
The runner does not check the earlyExit flag. Individual stages decide whether to skip their own logic based on the flag. This keeps the runner simple and gives each stage control over skip behavior — a stage might want to perform cleanup or logging even when early-exiting.
The pipeline supports extensibility through insertBefore(targetName, stage) and insertAfter(targetName, stage) methods. This is how the ModelFilter, CacheOrder, and Budget stages were added after the initial three-stage (Score-Resolve-Load) implementation without modifying existing code.
Stage 1: Score
The Score stage finds skills that match the current task. It reads the three input signals — intent, file, and context — and queries the skill index.
Skill Index Lookup
The SkillIndex is a prebuilt lookup structure that maps trigger patterns to skill names. When a skill is created with triggers like intents: ["vitest", "vitest workflow"], the index records that these phrases point to that skill. File triggers are matched as glob patterns. Context triggers match substrings of the current context description.
The index lookup is fast (O(1) for exact matches, O(n) for glob matching against file triggers) and produces a list of SkillIndexEntry objects containing the skill name, description, and trigger configuration.
TF-IDF Relevance Scoring
Beyond exact trigger matching, the Score stage uses the RelevanceScorer for fuzzy intent matching. The scorer builds a TF-IDF (Term Frequency-Inverse Document Frequency) corpus from all skill descriptions and trigger phrases using the natural library. When a user intent comes in, TF-IDF computes similarity scores against all indexed skills.
This provides semantic matching. A user saying "run my tests" can match a skill triggered by "vitest" if the skill's description mentions "testing" or "test runner." The default relevance threshold is 0.1 — skills scoring below this are excluded.
The scorer supports three match types:
- intent: Scored against user prompt text
- file: Scored against file paths being worked on
- context: Scored against session context strings (e.g., current GSD phase)
The output of the Score stage is an array of ScoredSkill objects, each with a name, relevance score, and match type. If no matches are found, the earlyExit flag is set to true and downstream stages skip their processing.
Stage 2: Resolve
The Resolve stage handles skill inheritance. Skills can extend other skills via the extends field in their gsd-skill-creator extension metadata. When skill B extends skill A, loading skill B should also include the content from skill A.
How the SkillResolver Works
For each scored skill, the SkillResolver performs these steps:
- Load the skill from the SkillStore
- Check for
extendsfield. If absent, the skill passes through unchanged. - Build a dependency graph by walking the inheritance chain. If skill C extends B, and B extends A, the graph captures both edges.
- Detect circular dependencies. If A extends B and B extends A (directly or transitively), the resolver throws an error with the cycle path:
"Circular dependency detected in skills: A -> B -> A". - Get the inheritance chain in root-first order:
[A, B, C](grandparent, parent, child). - Merge content and metadata from root to leaf.
Content Merging
Content is merged by concatenating parent body + separator + child body:
private mergeContent(parent: string, child: string): string {
return `${parent.trim()}\n\n---\n\n${child.trim()}`;
}
The --- separator is a standard markdown horizontal rule. This means the resolved skill content reads naturally as a single document with the parent's general guidelines followed by the child's specific instructions.
Metadata Merging
Metadata follows a "child wins" rule for identity fields (name, description) and Claude Code fields (disable-model-invocation, user-invocable, allowed-tools). But triggers are union-merged: the child inherits all trigger patterns from the parent and adds its own. This ensures the resolved skill activates on all the contexts that either skill individually would match.
Important: Learning data is explicitly not inherited. Each skill tracks its own learning state independently, even when extending a parent. This prevents learning leakage between related skills.
Conflict Detection
The Resolve stage also detects conflicts. If two scored skills resolve to overlapping configurations, the ConflictResolver determines the resolution strategy:
- Priority: Higher-scored skill wins (default)
- Merge: Combine content from both skills
- User-choice: Defer to the user
Stage 3: ModelFilter
The ModelFilter stage removes skills that are incompatible with the current AI model. Each skill can optionally specify model guidance in its metadata:
metadata:
extensions:
gsd-skill-creator:
model_guidance:
preferred: ["opus", "sonnet"]
minimumCapability: 2
The model capability hierarchy assigns numeric levels:
| Model | Capability Level | Characteristics |
|---|---|---|
| Opus | 3 | Highest reasoning, longest context, most expensive |
| Sonnet | 2 | Balanced performance and cost |
| Haiku | 1 | Fastest response, most economical, simpler reasoning |
A skill with minimumCapability: 2 is filtered out when running on Haiku (level 1). This prevents loading complex skills that require reasoning capabilities beyond what the current model can deliver — for example, a skill that instructs Claude to perform multi-step architectural analysis would not work well on Haiku.
The active model profile is passed in PipelineContext.modelProfile. When no profile is set, the filter stage passes all skills through unchanged.
Stage 4: CacheOrder
The CacheOrder stage reorders the remaining skills by recency of use. Skills that were loaded in recent sessions are promoted toward the front of the list. This serves two purposes:
- Relevance amplification: Recently-used skills are likely still relevant to the ongoing project work. If you used the vitest-workflow skill in your last 3 sessions, you are probably still working on tests.
- Budget priority: Since the Budget stage processes skills in order, recently-used skills are more likely to fit within the budget before it fills up. This gives cache-warm skills an implicit advantage over cold ones.
Cache ordering uses the SkillSession tracking data, which records when each skill was last loaded and how many times it has been activated in the current session window. Skills with no activation history retain their original order from the Score stage.
Stage 5: Budget
The Budget stage enforces the token and character budget — the portion of the context window allocated to skills. This is the critical gatekeeper that prevents skill loading from consuming too much of the available context.
Dual Budget System
There are two parallel budget constraints:
- Character budget (Claude Code layer): 15,000 chars per skill, 15,500 cumulative. This is the hard limit enforced by Claude Code itself — skills exceeding this are silently truncated or hidden. Controlled by the
SLASH_COMMAND_TOOL_CHAR_BUDGETenvironment variable. - Token budget (skill-creator layer): Configurable percentage of context window (default 3%). This is a softer limit managed by skill-creator. With a 200K token context, 3% gives a 6,000 token budget (~24,000 characters).
The Budget stage respects whichever constraint is tighter. In practice, the character budget (15,500 cumulative) is almost always the binding constraint because it is lower than the token-estimated character equivalent.
How Budget Evaluation Works
The BudgetValidator provides the core checking logic. For each skill:
- Read the SKILL.md file and parse it with
gray-matterto separate frontmatter from body - Count characters: description chars, body chars, total file chars
- Check the single-skill limit (15,000 chars). If exceeded, the skill receives error severity and is skipped.
- Add the skill's chars to the cumulative running total
- Check cumulative limit (15,500 chars). If exceeded, remaining skills are deferred.
Severity levels based on usage percentage:
| Usage | Severity | Meaning |
|---|---|---|
| <60% | ok | Plenty of room |
| 60-79% | info | Notable usage |
| 80-99% | warning | Approaching limit |
| ≥100% | error | Over budget |
Priority Tier Processing
Within the budget stage, skills are partitioned by their priority tier as defined in the active budget profile:
- Critical tier: These skills always load, up to the hard ceiling (e.g., 10% for gsd-executor). A
beautiful-commitsskill that must be present for every commit would be marked critical. Critical skills load even if they push past the standard budget — they are allowed up to the hard ceiling. - Standard tier: These load within the standard budget (e.g., 6% for gsd-executor). This is the default tier for all skills not explicitly assigned to another tier.
- Optional tier: These load only if budget remains after critical and standard skills. They are the first to be dropped when space is tight.
Skills not listed in any tier default to standard — this safe default ensures newly created skills are not accidentally excluded from loading.
Budget Stage Outputs
The budget stage writes three kinds of output to the context:
budgetSkipped: Array ofSkippedSkillobjects, each recording the skill name, tier, reason (budget_exceeded, hard_ceiling_reached, lower_priority, or model_mismatch), and estimated token countbudgetWarnings: Array ofBudgetWarningobjects at 50%, 80%, and 100% usage thresholds, each with a human-readable messagecontentCache: AMap<string, string>containing pre-read skill content. Since the Budget stage already reads each skill file to measure its size, it stores the content in this cache to avoid the Load stage re-reading the same files.
Stage 6: Load
The Load stage is the simplest in the pipeline. It reads skill content from disk (or the contentCache if the Budget stage pre-read it) and returns the final list of loaded skill contents.
For each skill that made it through all five previous stages:
- Check the contentCache. If the content is already cached (from the Budget stage's size measurement), use it directly.
- Otherwise, read the SKILL.md file from disk.
- Add the skill name to the
loadedarray. - If the file read fails (missing file, permission error), add the skill name to the
skippedarray instead.
The output arrays — loaded and skipped — are the final pipeline result returned to the caller for injection into the Claude Code session.
Concrete Walkthrough: Loading a Skill
Let us trace a concrete example through all 6 stages. Assume you have these skills installed:
| Skill | Triggers | Chars | Tier | Notes |
|---|---|---|---|---|
vitest-workflow | intent: "vitest"; file: "*.test.ts" | 2,100 | standard | — |
beautiful-commits | intent: "commit"; context: "git" | 3,800 | critical | — |
prisma-migrate | intent: "prisma"; file: "schema.prisma" | 1,900 | standard | extends: database-base |
database-base | intent: "database" | 1,200 | standard | — |
react-patterns | file: "*.tsx" | 4,500 | optional | minimumCapability: 3 |
Scenario: "run the tests for the pattern analyzer"
Stage 1 — Score: The intent "run the tests for the pattern analyzer" is matched against skill triggers and the TF-IDF corpus. The word "tests" overlaps with vitest-workflow's description. File matching against pattern-analyzer.test.ts also triggers vitest-workflow. beautiful-commits scores below the 0.1 threshold on the word "tests." Result: scoredSkills = [{name: "vitest-workflow", score: 0.72, matchType: "intent"}]. One skill matched.
Stage 2 — Resolve: vitest-workflow has no extends field. It passes through unchanged. No conflicts. Result: resolvedSkills = [{name: "vitest-workflow", score: 0.72}].
Stage 3 — ModelFilter: vitest-workflow has no model_guidance. It passes through unconditionally. Result: unchanged.
Stage 4 — CacheOrder: vitest-workflow was loaded in the previous session 2 hours ago. It is already first (and only), so the order does not change. Result: unchanged.
Stage 5 — Budget: The Budget stage reads vitest-workflow (2,100 characters). Cumulative: 2,100 / 15,500 = 13.5%. Severity: ok. No warnings triggered. Content is cached in contentCache. Result: no skills skipped.
Stage 6 — Load: vitest-workflow content is retrieved from contentCache and returned. Final: loaded = ["vitest-workflow"], skipped = [], budgetSkipped = [].
Scenario: "run the tests, then commit the changes"
Now suppose you say "run the tests, then commit the changes."
Stage 1 — Score: TF-IDF matches both vitest-workflow (score: 0.72 on "tests") and beautiful-commits (score: 0.85 on "commit"). Two skills matched.
Stage 2 — Resolve: Neither skill has extends. Both pass through. No conflicts.
Stage 3 — ModelFilter: Both pass (no model_guidance).
Stage 4 — CacheOrder: Both were used recently. beautiful-commits (used 30 minutes ago) moves ahead of vitest-workflow (used 2 hours ago).
Stage 5 — Budget: beautiful-commits is critical tier, so it loads first regardless of order: 3,800 chars. Then vitest-workflow (standard tier): 2,100 chars. Cumulative: 5,900 / 15,500 = 38%. Severity: ok.
Stage 6 — Load: Both skills loaded. Final: loaded = ["beautiful-commits", "vitest-workflow"].
Scenario: Budget overflow with model filtering
Now suppose someone says "edit the React component and run the database migration." On Sonnet (capability 2):
Stage 1 — Score: Matches react-patterns (file: "*.tsx"), prisma-migrate (intent: "migration"), and database-base (intent: "database"). Three skills.
Stage 2 — Resolve: prisma-migrate extends database-base. The resolver merges them: combined content is 1,200 + 1,900 = ~3,100 chars (plus separator). react-patterns has no extends.
Stage 3 — ModelFilter: react-patterns has minimumCapability: 3. Current model is Sonnet (capability 2). react-patterns is filtered out. Two skills remain.
Stage 5 — Budget: prisma-migrate (resolved): ~3,100 chars. Cumulative: 20%. OK.
Stage 6 — Load: loaded = ["prisma-migrate"], budgetSkipped = [{name: "react-patterns", reason: "model_mismatch"}].
Usage Examples
Example 1: Checking Budget Status
Use the CLI to see current budget consumption across all installed skills:
# Check cumulative budget across all installed skills
npx skill-creator status
# Output:
Budget: [########............] 38% (5,900 / 15,500 chars)
beautiful-commits: 3,800 chars (24.5%)
vitest-workflow: 2,100 chars (13.5%)
2 skills installed, 0 hidden
With a budget profile applied to see what the gsd-executor agent would load:
# Check budget for a specific agent profile
npx skill-creator status --profile gsd-executor
# Output:
Installed: 13,500 chars across 5 skills
Loadable: 9,000 chars (4 of 5 skills fit)
Budget: [###########.........] 58% (9,000 / 15,500 chars)
beautiful-commits: 3,800 chars (24.5%) [critical]
vitest-workflow: 2,100 chars (13.5%) [standard]
prisma-migrate: 1,900 chars (12.3%) [standard]
database-base: 1,200 chars (7.7%) [standard]
react-patterns: 4,500 chars (29.0%) [optional] -- DEFERRED (model mismatch)
JSON output for programmatic consumption:
npx skill-creator status --json
# Output:
{
"totalChars": 13500,
"budget": 15500,
"usagePercent": 87.1,
"severity": "warning",
"skills": [
{ "name": "react-patterns", "totalChars": 4500 },
{ "name": "beautiful-commits", "totalChars": 3800 },
{ "name": "vitest-workflow", "totalChars": 2100 },
{ "name": "prisma-migrate", "totalChars": 1900 },
{ "name": "database-base", "totalChars": 1200 }
],
"hiddenCount": 0,
"installedTotal": 13500,
"loadableTotal": 13500
}
Example 2: Setting Up Skill Inheritance
Create a parent skill with shared content, then a child that extends it:
# Parent skill: .claude/commands/database-base/SKILL.md
---
name: database-base
description: >-
Base patterns for database operations.
Use when working with databases, migrations, or schemas.
metadata:
extensions:
gsd-skill-creator:
enabled: true
triggers:
intents:
- database
- migration
threshold: 0.4
---
# Database Operations Base
## Connection Patterns
Always verify the connection string before running migrations.
Check `.env` for DATABASE_URL.
## Safety Rules
- Never run destructive migrations without a backup
- Always test migrations in development first
- Review the generated SQL before applying to production
# Child skill: .claude/commands/prisma-migrate/SKILL.md
---
name: prisma-migrate
description: >-
Prisma migration workflow.
Use when running prisma commands or editing schema.prisma.
metadata:
extensions:
gsd-skill-creator:
enabled: true
extends: database-base
triggers:
intents:
- prisma
- prisma migrate
files:
- schema.prisma
- prisma/migrations/**
threshold: 0.5
---
# Prisma Migration Workflow
## Steps
1. Edit schema.prisma with your model changes
2. Run `npx prisma migrate dev --name descriptive-name`
3. Verify the generated SQL in prisma/migrations/
4. Run `npx prisma generate` to update the client
5. Update seed data if needed
When prisma-migrate is loaded through the pipeline, the Resolve stage merges the content:
# Resolved content (parent first, separator, then child):
# Database Operations Base
## Connection Patterns
Always verify the connection string before running migrations.
Check `.env` for DATABASE_URL.
## Safety Rules
- Never run destructive migrations without a backup
- Always test migrations in development first
- Review the generated SQL before applying to production
---
# Prisma Migration Workflow
## Steps
1. Edit schema.prisma with your model changes
2. Run `npx prisma migrate dev --name descriptive-name`
3. Verify the generated SQL in prisma/migrations/
4. Run `npx prisma generate` to update the client
5. Update seed data if needed
The resolved triggers are the union: intents: ["database", "migration", "prisma", "prisma migrate"], files: ["schema.prisma", "prisma/migrations/**"]. The child's threshold (0.5) takes precedence over the parent's (0.4).
Example 3: Configuring Priority Tiers
Assign skills to priority tiers in your project configuration:
# In .planning/config.json
{
"budget_profiles": {
"gsd-executor": {
"tiers": {
"critical": ["beautiful-commits"],
"standard": ["vitest-workflow", "prisma-migrate", "database-base"],
"optional": ["react-patterns", "documentation-style"]
}
},
"gsd-planner": {
"tiers": {
"critical": [],
"standard": ["documentation-style"],
"optional": ["react-patterns"]
}
}
},
"token_budget": {
"cumulative_char_budget": 15500,
"profile_budgets": {
"executor": 16000,
"planner": 12000
}
}
}
With this configuration:
beautiful-commitsalways loads for the executor (up to 10% hard ceiling)- Standard skills load within the 6% standard budget
- Optional skills like
react-patternsonly load if budget remains - The planner agent has a lower cumulative budget (12,000 chars) and different tier assignments
Example 4: Adding a Custom Pipeline Stage
import { SkillPipeline, PipelineContext, PipelineStage } from 'gsd-skill-creator';
// Custom stage that logs which skills pass through
class AuditStage implements PipelineStage {
readonly name = 'Audit';
async process(context: PipelineContext): Promise<PipelineContext> {
console.log(`[Audit] Resolved: ${context.resolvedSkills.length}`);
console.log(`[Audit] Loaded: ${context.loaded.length}`);
console.log(`[Audit] Skipped: ${context.budgetSkipped.length}`);
for (const warning of context.budgetWarnings) {
console.warn(`[Audit] Warning: ${warning.message}`);
}
return context;
}
}
// Insert after the Load stage
pipeline.insertAfter('Load', new AuditStage());
// The pipeline now runs: Score -> Resolve -> ModelFilter
// -> CacheOrder -> Budget -> Load -> Audit
PipelineContext as a Data Contract
The PipelineContext interface is the API contract between stages. Key design decisions:
- No generic types: The context has a fixed shape. This simplifies stage implementations and makes the data flow explicit. Each stage's preconditions and postconditions are documented in comments.
- Read-only inputs:
intent,file,context, andmodelProfileare markedreadonly. Stages must not modify inputs. - earlyExit is a flag, not a mechanism: The runner does not check earlyExit. Stages check it themselves and decide whether to skip. This allows stages to perform cleanup or logging even when early-exiting.
- contentCache prevents double reads: The Budget stage reads skill content to measure size. It stores the content in a Map so the Load stage can retrieve it without re-reading files.
- getReport is caller-provided: The context includes a
getReport()function that returns aSessionReportwith active skills, total tokens, budget limit, usage percentage, remaining budget, token tracking entries, and flagged skills. This is injected by the caller (usuallySkillSession) and provides clean dependency inversion.
interface PipelineContext {
// Inputs (read-only)
readonly intent?: string;
readonly file?: string;
readonly context?: string;
readonly modelProfile?: string;
// Intermediate results
matches: SkillIndexEntry[];
scoredSkills: ScoredSkill[];
resolvedSkills: ScoredSkill[];
conflicts: ConflictResult;
// Outputs
loaded: string[];
skipped: string[];
budgetSkipped: SkippedSkill[];
budgetWarnings: BudgetWarning[];
contentCache: Map<string, string>;
// Control
earlyExit: boolean;
getReport: () => SessionReport;
}
TF-IDF Scoring Implementation
The RelevanceScorer uses the natural library's TF-IDF implementation. Each skill becomes a document built from its description concatenated with trigger intent and context strings. The TF-IDF index is built once when the pipeline initializes (indexSkills()) and remains static for the session.
Scoring via tfidfs(query, callback) iterates all documents, computing the TF-IDF score for the query against each. Results below the relevance threshold (default 0.1) are filtered out. The scorer also provides getTopSkills(query, n, threshold) for quick top-N retrieval.
Important consideration: Adding or removing skills requires re-indexing. The TF-IDF corpus does not support incremental updates in the natural library — a full rebuild is needed. For typical installations (5-20 skills), this takes under 5ms.
Token Counting: API vs Estimate
The TokenCounter provides two strategies:
- API strategy: Calls
Anthropic.messages.countTokens()with modelclaude-sonnet-4-5-20250929. Exact counts, requires API key, costs API usage. Used whenuseApi: trueis passed. - Estimate strategy: Uses
gpt-tokenizerlibrary (encode()function). Free, offline, ~95% accuracy for English text. The default strategy.
Token counts are cached using a simple hash of the content string (32-bit integer hash converted to base-36). The cache is stored in memory and cleared via clearCache() when skills change. Budget calculation: budget = contextWindowSize * budgetPercent.
Loading Projection for Budget Profiles
When a BudgetProfile is provided to checkCumulative(), the validator computes a loading projection via projectLoading(). This separates the installed skill inventory from what would actually load under the given profile. The projection returns:
loaded: Skills that fit within the profile's budget limitsdeferred: Skills that would be deferred due to budget or tier constraintsloadedTotal: Total characters of loaded skills
The dual-view display (formatBudgetDisplay()) uses this projection to show "Installed: X chars across N skills" vs "Loadable: Y chars (M of N skills fit)" when the two totals differ.
Budget Monitoring and Alerts
The budget system provides three warning thresholds for operational monitoring:
- 50% warning: Informational. The skill inventory is growing. Consider reviewing which skills are still needed.
- 80% warning: Approaching limit. New skills may not fit. Review optional-tier skills for removal or consolidation.
- 100% error: Budget exceeded. Some skills are being hidden by Claude Code silently. This degrades the user experience because expected skills may not be available.
Monitor budget status programmatically:
# Check if budget is in error state
npx skill-creator status --json | jq '.severity'
# Returns: "ok", "info", "warning", or "error"
# Get count of hidden skills
npx skill-creator status --json | jq '.hiddenCount'
# Returns: 0 (healthy) or N (skills being silently dropped)
# Get the largest skill for size optimization
npx skill-creator status --json | jq '.skills[0]'
# Returns the skill with the most characters
# Monitor budget trend over time (append to log)
echo "$(date -Iseconds) $(npx skill-creator status --json | jq -c '{severity, usagePercent, hiddenCount}')" >> /var/log/skill-budget.log
Character Budget vs Token Budget
There are two parallel budget systems, which can cause confusion during troubleshooting:
| Aspect | Character Budget | Token Budget |
|---|---|---|
| Layer | Claude Code (enforced externally) | skill-creator (enforced internally) |
| Per-skill limit | 15,000 chars | N/A |
| Cumulative limit | 15,500 chars | contextSize * budgetPercent |
| Override | SLASH_COMMAND_TOOL_CHAR_BUDGET | .planning/config.json |
| Enforcement | Skills silently truncated/hidden | Skills deferred with warning |
| Typical value | 15,500 chars | ~24,000 chars (200K * 3%) |
The character budget is almost always the binding constraint. When troubleshooting "why is my skill not loading," check the character budget first.
Agent Profile Configuration
Budget profiles are configured per GSD agent role. Override defaults in .planning/config.json under the budget_profiles key. Profile-specific cumulative budgets use short names in the config (e.g., "executor": 20000 for gsd-executor). The getCumulativeBudgetForProfile() method strips the gsd- prefix automatically for lookup.
Each profile defines:
budgetPercent: Standard budget as a fraction of context windowhardCeilingPercent: Absolute maximum including critical tier overflowtiers: Skill name partitioning into critical, standard, and optionalthresholds: Which budget warnings to fire (warn50, warn80, warn100)
Performance Characteristics
The pipeline is designed for fast startup. Typical performance:
| Stage | Typical Latency | Scaling Factor |
|---|---|---|
| Score (TF-IDF) | <5ms | Number of indexed skills |
| Resolve (inheritance) | <10ms per chain | Depth of inheritance |
| ModelFilter | <1ms | Number of scored skills |
| CacheOrder | <1ms | Number of scored skills |
| Budget | <20ms | Number of skill files read |
| Load | <5ms (cached), <20ms (disk) | Number of loaded skills |
| Total | <50ms | 5-10 installed skills |
If pipeline performance degrades, investigate:
- Number of installed skills (more = more TF-IDF documents)
- Depth of inheritance chains (each level adds a disk read in the Resolve stage)
- Large skill files (slow character counting in the Budget stage)
- Disk I/O latency (skills are read from disk, not cached between sessions)
- Network latency if using the Anthropic API for token counting (rare; offline estimation is the default)
Reducing Skill Sizes
When skills approach the character budget, the generateSuggestions() function provides actionable advice:
- Descriptions over 200 characters: shorten by the excess amount
- Bodies over 10,000 characters: move content to reference files (
@filereferences) and consider splitting into multiple focused skills - Bodies over 5,000 characters with excess: move examples or verbose sections to reference files
Progressive disclosure (DISC-01/DISC-02) automates this during skill generation — the ContentDecomposer splits large skills into a main SKILL.md and separate reference/script files. For manually-written skills that have grown large, consider applying the same pattern manually.
Troubleshooting
A skill is not loading even though it should match
Symptom: You say something that should trigger a skill, but the skill is not loaded.
Causes and fixes:
- Trigger mismatch: Check the skill's trigger configuration. Intent triggers require word overlap with your prompt. If your trigger is
"prisma migrate"but you said"run a database migration", TF-IDF may not score it high enough. Add broader intent phrases like"database migration"to the trigger list. - Below relevance threshold: The default threshold is 0.1. If TF-IDF scores the skill at 0.08, it will not match. Lower the skill's trigger threshold or add more descriptive text to the skill's description field.
- Budget exceeded: The skill may have been filtered out by the Budget stage. Run
npx skill-creator statusto check cumulative usage. If your skills total exceeds 15,500 chars, some will be hidden. - Model filtered: If the skill has
model_guidance.minimumCapabilityset and the current model does not meet it, the ModelFilter stage removes it silently. - Disabled: Check the skill's
gsd-skill-creator.enabledfield. If set tofalse, the skill is excluded from the index entirely. - earlyExit: If no skills matched in Stage 1, all subsequent stages skip. Check that at least one skill has triggers that match your intent/file/context.
Skills are being silently hidden by Claude Code
Symptom: npx skill-creator status shows hiddenCount > 0.
Causes and fixes:
- Your cumulative skill content exceeds 15,500 characters. Claude Code silently drops skills that do not fit.
- Review the per-skill size breakdown in the status output. Identify the largest skills.
- Use progressive disclosure: move detailed content to reference files that are only loaded when needed.
- Split large skills into smaller, focused skills with inheritance so only relevant parts load.
- Assign low-priority skills to the
optionaltier so they are dropped gracefully instead of randomly. - Consider removing skills that are no longer needed. Run
npx skill-creator statusand review each skill's contribution to the budget.
Circular dependency error during resolve
Symptom: Error message: Circular dependency detected in skills: A -> B -> A.
Causes and fixes:
- Two or more skills reference each other in their
extendsfields, creating an infinite loop. - The error message shows the cycle path. Remove one of the
extendsreferences to break the cycle. - Typically, refactor the shared content into a common parent skill that both others extend independently.
Budget warnings appear but all skills are loading
Symptom: The status command shows 80%+ usage but no skills appear in budgetSkipped.
Causes and fixes:
- This is an informational warning, not an error. All skills fit within budget, but you are approaching the limit.
- Adding a new skill may push you over. Audit your skills now: remove or consolidate unused ones.
- Check if any skills have grown large from refinement cycles. Use
npx skill-creator statusto see per-skill sizes.
Token count estimates seem inaccurate
Symptom: Budget calculations do not match expectations; skills load or are skipped unexpectedly.
Causes and fixes:
- The offline token estimator uses
gpt-tokenizer, which approximates Claude's tokenization at ~95% accuracy. It can diverge for code-heavy content or non-English text. - For exact counts, set an Anthropic API key to enable the
countTokensAPI endpoint. - Remember that the character budget (15,000/15,500) is enforced by Claude Code independently of token counts. Even with exact token estimates, the character limit may be the binding constraint. Focus on character counts when troubleshooting.

