Skill Management

How pair resolves, transforms, and renames skills, path resolution, the transformation pipeline, and naming conventions.

pair skills are SKILL.md files that live in the Knowledge Base and get distributed to AI tool directories. This page explains the three mechanisms that make this work: resolution, transformation, and renaming.

Skill Resolution

The Problem

Skills are authored in a category-based directory structure (capability/verify-quality/SKILL.md) but AI tools expect them in a flat target directory (.claude/skills/). The CLI needs to locate the right source files and map them to the right targets.

How It Works

Resolution is registry-based, not runtime file scanning. The config.json declares the source directory, target directories, and transform rules:

{
  "skills": {
    "source": ".skills",
    "targets": [
      { "path": ".claude/skills/", "mode": "canonical" },
      { "path": ".github/skills/", "mode": "symlink" }
    ]
  }
}

Source Types

Skills come from three sources:

SourceLocationExample
Installed KBKB dataset .skills/ directory.skills/capability/verify-quality/SKILL.md
Local overrideProject .pair/ directoryCustom skills added by the team
ComposedReferenced inside another SKILL.md/pair-capability-verify-quality invoked by /pair-process-implement

Resolution Order

  1. CLI reads config.json to find the skills registry entry
  2. Scans the source directory (.skills/) for all SKILL.md files
  3. Applies naming transforms (flatten + prefix)
  4. Copies to canonical target (.claude/skills/)
  5. Creates symlinks for secondary targets (.github/skills/, .cursor/skills/, etc.)

Transformation Pipeline

The Problem

A raw SKILL.md file contains relative links, short skill names, and category-based paths. After installation, these need to work in a flat directory structure where all skills are siblings.

Pipeline Stages

The pipeline runs during pair-cli install and pair-cli update:

Stage 1: Copy with naming transforms

Source:  .skills/capability/verify-quality/SKILL.md
Target:  .claude/skills/pair-capability-verify-quality/SKILL.md

Stage 2: Frontmatter sync

# Before (source)
name: verify-quality
 
# After (installed)
name: pair-capability-verify-quality

Stage 3: Link rewriting

<!-- Before: relative link in category structure -->
See [record-decision](../../capability/record-decision/SKILL.md)
 
<!-- After: sibling link in flat structure -->
See [record-decision](../pair-capability-record-decision/SKILL.md)

Stage 4: Skill reference rewriting

<!-- Before -->
Composes /verify-quality and /record-decision.
 
<!-- After -->
Composes /pair-capability-verify-quality and /pair-capability-record-decision.

Stage 5: Multi-target distribution

.claude/skills/pair-capability-verify-quality/  (canonical - physical copy)
.github/skills/pair-capability-verify-quality/  (symlink → .claude/skills/...)
.cursor/skills/pair-capability-verify-quality/  (symlink → .claude/skills/...)

Stage 6: Cross-registry propagation

After all registries are installed, skill references in non-skills files (like AGENTS.md) are also rewritten:

<!-- AGENTS.md before -->
Run /next to get started.
 
<!-- AGENTS.md after -->
Run /pair-next to get started.

Concrete Example

Here's a full before/after for the implement skill:

Before (source):

Path: .skills/process/implement/SKILL.md
Frontmatter: name: implement
Body: Composes /verify-quality and /record-decision.
Link: See [verify-quality](../../capability/verify-quality/SKILL.md)

After (installed):

Path: .claude/skills/pair-process-implement/SKILL.md
Frontmatter: name: pair-process-implement
Body: Composes /pair-capability-verify-quality and /pair-capability-record-decision.
Link: See [verify-quality](../pair-capability-verify-quality/SKILL.md)

Renaming Conventions

The Problem

AI tools invoke skills by directory name (e.g., /implement). Without namespacing, skill names could collide with other tools' commands. Prefixing with pair- and the category creates unique, discoverable names.

Naming Rules

The transform is deterministic: {prefix}-{category}-{skill-name}

With the default config (prefix: "pair", flatten: true):

Source PathInstalled DirectorySlash Command
next/ (top-level)pair-next//pair-next
process/implement/pair-process-implement//pair-process-implement
process/review/pair-process-review//pair-process-review
capability/verify-quality/pair-capability-verify-quality//pair-capability-verify-quality
capability/record-decision/pair-capability-record-decision//pair-capability-record-decision
process/review/references/ (sub-dir of a skill)pair-process-review/references/none, content, not a skill

Collision Detection

If two different source paths flatten to the same installed name (e.g. a/b/ and a-b/ both becoming a-b/), install fails outright with an explicit error listing the collision instead of silently overwriting one skill with the other.

Three Namespace Prefixes

PrefixCountPurpose
pair-1Top-level skills (only next)
pair-process-9Workflow orchestration skills
pair-capability-25Atomic operation skills

Transform Functions

The naming transform applies two functions in order:

  1. Flatten: Replace / with -, bounded to the registry's entry depth (flattenDepth: 2 for skills: see Configuration)

    • capability/verify-quality → capability-verify-quality
    • process/review/references → process-review/references (a skill's own sub-directory is preserved, not flattened into a sibling)
  2. Prefix: Prepend the prefix with - separator (only the top-level segment)

    • capability-verify-quality → pair-capability-verify-quality
    • process-review/references → pair-process-review/references
// Flatten: joins the first `flattenDepth` segments with '-' and preserves the
// rest as a real sub-path. Omit flattenDepth to join every separator.
function flattenPath(dirName: string, flattenDepth?: number): string {
  const segments = dirName.split('/')
  if (flattenDepth === undefined || segments.length <= flattenDepth) {
    return segments.join('-')
  }
  return [segments.slice(0, flattenDepth).join('-'), ...segments.slice(flattenDepth)].join('/')
}
 
// Prefix: prepends prefix with '-' separator
function prefixPath(dirName: string, prefix: string): string {
  return `${prefix}-${dirName}`
}

Reference Rewriting Rules

Skill references (slash commands in markdown) are rewritten using boundary-aware matching:

  • Only references with a leading / are rewritten (e.g., /next but not next)
  • Longest matches are processed first to avoid partial replacements
  • Boundaries include: start/end of line, whitespace, backticks, quotes, parentheses, pipes

Examples:

InputOutput
`/next``/pair-next`
Composes /verify-quality and /record-decision.Composes /pair-capability-verify-quality and /pair-capability-record-decision.
invoke /implement immediatelyinvoke /pair-process-implement immediately
name: pair-next (no leading slash)name: pair-next (unchanged)
path/next/page (mid-path)path/next/page (unchanged)

Fenced Code Blocks Are Skipped

Rewriting is applied line-by-line and tracks fenced code blocks (``` or ~~~). Content inside a fence is left untouched: a skill name quoted as example text isn't a live invocation. Inline single-backtick code spans (`/next`) are still rewritten; only triple-backtick/tilde blocks are excluded.

Run `/next` to start.                    <- rewritten (inline code span)
[fenced code block containing "/next"]   <- left as-is
Then run /implement.                     <- rewritten (prose)

The relative-link rewriter (Stage 3) gets this for free: it walks a real markdown AST, so links inside fenced or inline code were never extracted as links in the first place.

Idempotent Updates and Prefix Changes

The Problem

pair-cli update re-derives skill bodies from source on every run, so a rename (e.g. a prefix change in pair.config.json) always produces the correct new skill files. But two kinds of content are not re-derived from source every run:

  • Files copied with add behavior (e.g. adoption docs): never overwritten once they exist, so a stale reference baked in during an earlier install (/pair-next) is never revisited by a plain copy-and-rewrite pass.
  • Anything a previous run wrote using the old prefix, where the current config only knows the new one.

Guessing at the old prefix by string-matching ("pair-" → "foo-") is explicitly avoided: it produces false positives on unrelated content that happens to start with the same string.

The Fix: A Recorded Mapping

Each install/update writes a small manifest (.pair/.skill-name-map.json) recording this run's {shortName: installedName} map. The next run reads it back and computes an exact transition map (oldInstalledName -> newInstalledName) for every skill whose installed name changed, then applies that transition on top of the normal short-name rewrite when reconciling non-skill registries. The manifest is then rewritten with the current mapping.

This is why install -> update -> update is byte-stable, and why changing prefix between two runs correctly:

  • removes the old flattened directory (e.g. pair-catalog-next/) instead of leaving it orphaned alongside the new one,
  • rewrites already-installed references (including ones living in add-behavior files) to the new name.

Removed or Disabled Skills

If a skill disappears from the source registry between runs, its old installed name has no valid new target. Any remaining /oldName invocation is left exactly as-is, and the install/update report logs a warning so it can be cleaned up by hand.

External KBs

The same registry-build, rewrite, and manifest pipeline runs regardless of where the dataset comes from: the official KB, a project override, or an external KB installed via --source. No source-side restructuring is required or supported; the nested .skills/<type>/<name>/ layout is flattened, prefixed, and rewritten identically either way.

Windows Compatibility

On Windows, symlink targets fall back to copy mode because symlinks are not reliably supported. This means secondary targets (.github/skills/, .cursor/skills/) get independent copies instead of symlinks.

Migration Notes

map-subdomains, map-contexts: process → capability

Reclassified so they can be invoked scoped by any planning/refinement skill, not just as a standalone lifecycle step:

BeforeAfter
Source path.skills/process/map-subdomains/, .skills/process/map-contexts/.skills/capability/map-subdomains/, .skills/capability/map-contexts/
Installed command/pair-process-map-subdomains, /pair-process-map-contexts/pair-capability-map-subdomains, /pair-capability-map-contexts
$scopeOptional (all default, or single)Required, items/areas the caller touched; all is bootstrap-only
No DDD artifactsPrerequisite check HALTsFalls back to "system areas" (services/modules): no HALT

No code change is required in apps/pair-cli: the skills registry (config.json) is category-agnostic; moving a skill between process/ and capability/ in the dataset is picked up automatically by the flatten+prefix transform on the next pair-cli install/pair-cli update.