Activation Pipeline Guide

Quick Reference

Features Covered

FeatureModuleVersion
6-Stage Pipelinesrc/application/skill-pipeline.tsv1.8
Token Budgetingsrc/validation/budget-validation.tsv1.8
Model Profilessrc/application/budget-profiles.tsv1.11
Skill Inheritancesrc/composition/skill-resolver.tsv1.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:

AgentStandardCeiling
gsd-executor6%10%
gsd-planner5%8%
gsd-verifier5%8%
gsd-debugger6%10%
gsd-researcher5%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 stages
  • PipelineStage — Interface for custom stages
  • ScoredSkill — Skill with relevance score
  • ConflictResult — Conflict detection result
  • SkillResolution — Merged inheritance result
  • BudgetProfile — Per-agent budget config
  • BudgetCheckResult — Single skill budget result
  • CumulativeBudgetResult — All-skills budget result
  • SkippedSkill — Why a skill was not loaded
  • BudgetWarning — Threshold warning object
  • TokenCountResult — Count with source tracking
  • ModelGuidance — 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

For Students & Teachers

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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: earlyExit flag 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:

  1. Load the skill from the SkillStore
  2. Check for extends field. If absent, the skill passes through unchanged.
  3. Build a dependency graph by walking the inheritance chain. If skill C extends B, and B extends A, the graph captures both edges.
  4. 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".
  5. Get the inheritance chain in root-first order: [A, B, C] (grandparent, parent, child).
  6. 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:

ModelCapability LevelCharacteristics
Opus3Highest reasoning, longest context, most expensive
Sonnet2Balanced performance and cost
Haiku1Fastest 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:

  1. 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_BUDGET environment variable.
  2. 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:

  1. Read the SKILL.md file and parse it with gray-matter to separate frontmatter from body
  2. Count characters: description chars, body chars, total file chars
  3. Check the single-skill limit (15,000 chars). If exceeded, the skill receives error severity and is skipped.
  4. Add the skill's chars to the cumulative running total
  5. Check cumulative limit (15,500 chars). If exceeded, remaining skills are deferred.

Severity levels based on usage percentage:

UsageSeverityMeaning
<60%okPlenty of room
60-79%infoNotable usage
80-99%warningApproaching limit
≥100%errorOver budget

Priority Tier Processing

Within the budget stage, skills are partitioned by their priority tier as defined in the active budget profile:

  1. Critical tier: These skills always load, up to the hard ceiling (e.g., 10% for gsd-executor). A beautiful-commits skill 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.
  2. 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.
  3. 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 of SkippedSkill objects, each recording the skill name, tier, reason (budget_exceeded, hard_ceiling_reached, lower_priority, or model_mismatch), and estimated token count
  • budgetWarnings: Array of BudgetWarning objects at 50%, 80%, and 100% usage thresholds, each with a human-readable message
  • contentCache: A Map<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:

  1. Check the contentCache. If the content is already cached (from the Budget stage's size measurement), use it directly.
  2. Otherwise, read the SKILL.md file from disk.
  3. Add the skill name to the loaded array.
  4. If the file read fails (missing file, permission error), add the skill name to the skipped array 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:

SkillTriggersCharsTierNotes
vitest-workflowintent: "vitest"; file: "*.test.ts"2,100standard—
beautiful-commitsintent: "commit"; context: "git"3,800critical—
prisma-migrateintent: "prisma"; file: "schema.prisma"1,900standardextends: database-base
database-baseintent: "database"1,200standard—
react-patternsfile: "*.tsx"4,500optionalminimumCapability: 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-commits always loads for the executor (up to 10% hard ceiling)
  • Standard skills load within the 6% standard budget
  • Optional skills like react-patterns only 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
For Professionals & Developers

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, and modelProfile are marked readonly. 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 a SessionReport with active skills, total tokens, budget limit, usage percentage, remaining budget, token tracking entries, and flagged skills. This is injected by the caller (usually SkillSession) 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 model claude-sonnet-4-5-20250929. Exact counts, requires API key, costs API usage. Used when useApi: true is passed.
  • Estimate strategy: Uses gpt-tokenizer library (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 limits
  • deferred: Skills that would be deferred due to budget or tier constraints
  • loadedTotal: 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.

For SysAdmins & Operators

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:

AspectCharacter BudgetToken Budget
LayerClaude Code (enforced externally)skill-creator (enforced internally)
Per-skill limit15,000 charsN/A
Cumulative limit15,500 charscontextSize * budgetPercent
OverrideSLASH_COMMAND_TOOL_CHAR_BUDGET.planning/config.json
EnforcementSkills silently truncated/hiddenSkills deferred with warning
Typical value15,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 window
  • hardCeilingPercent: Absolute maximum including critical tier overflow
  • tiers: Skill name partitioning into critical, standard, and optional
  • thresholds: Which budget warnings to fire (warn50, warn80, warn100)

Performance Characteristics

The pipeline is designed for fast startup. Typical performance:

StageTypical LatencyScaling Factor
Score (TF-IDF)<5msNumber of indexed skills
Resolve (inheritance)<10ms per chainDepth of inheritance
ModelFilter<1msNumber of scored skills
CacheOrder<1msNumber of scored skills
Budget<20msNumber of skill files read
Load<5ms (cached), <20ms (disk)Number of loaded skills
Total<50ms5-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 (@file references) 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 status to check cumulative usage. If your skills total exceeds 15,500 chars, some will be hidden.
  • Model filtered: If the skill has model_guidance.minimumCapability set and the current model does not meet it, the ModelFilter stage removes it silently.
  • Disabled: Check the skill's gsd-skill-creator.enabled field. If set to false, 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 optional tier so they are dropped gracefully instead of randomly.
  • Consider removing skills that are no longer needed. Run npx skill-creator status and 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 extends fields, creating an infinite loop.
  • The error message shows the cycle path. Remove one of the extends references 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 status to 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 countTokens API 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.

Related Capabilities