MINERVA Phase C-7 — 스킬 레지스트리 (등록·검색·버전·의존성 추적)

150+ 스킬을 관리하려면 단일 진실의 카탈로그가 필요하다 — 검색·resolve·의존성 그래프·CI/CD 통합

C27이 스킬 단위를 정의했다면, C28은 그 단위들을 하나로 묶는 카탈로그 시스템이다. 본 편은 레지스트리 데이터 모델, 등록 API, 검색·discovery 패턴(이름·태그·임베딩), 버전 resolution 전략, 의존성 그래프(skill→tool·skill→skill·agent→skill), git+DB hybrid 저장, 캐싱 전략, CI/CD 통합, registry SPOF·cache invalidation·dependency cycle 같은 함정을 정리한다.

Agent
저자

Kwangmin Kim

공개

2026년 05월 06일

1 왜 레지스트리인가

C27 스킬 정의가 한 스킬의 정의를 표준화했다면, 150+개 스킬이 운영에 들어가기 시작하면 다음이 필요해진다.

운영 요구 레지스트리 없이의 결과
LLM이 “어떤 스킬을 호출할지” 동적 선택 코드에 hardcode된 if/elif 폭증
새 스킬 도입 시 기존과 중복 검사 사람 review가 모든 PR마다
한 스킬을 사용하는 모든 에이전트 추적 변경 영향 분석 불가
스킬 버전 결정 (latest·active·pinned) 런타임 에러·일관성 깨짐
스킬 사용 통계 (분기 audit) 어느 스킬을 deprecate할지 결정 못 함

레지스트리는 단일 진실의 카탈로그다. 모든 스킬·관계·메타가 한 곳에서 조회 가능하고, 변경은 명시적 절차로만.

2 데이터 모델

# app/skills/registry/models.py
from datetime import datetime
from pydantic import BaseModel
from typing import Literal


class SkillRecord(BaseModel):
    skill_id: str                            # "summarize_doc"
    version: str                              # "1.3.0"
    stage: Literal["draft", "canary", "staged", "active", "deprecated", "retired"]
    traffic_pct: float                        # canary·staged 시 비중
    owner: str
    description: str
    tags: list[str]                           # ["summarization", "multi-step"]

    # 의존성
    tools_required: list[str]
    tools_optional: list[str]
    skills_called: list[str]                  # composition (C29)
    models_compatible: list[str]              # ["gpt-4o", "claude-3.5"]

    # 위치
    git_path: str                             # "skills/summarize_doc/SKILL.md"
    git_sha: str                              # 정확한 commit hash

    # 메타
    created_at: datetime
    activated_at: datetime | None = None
    deprecated_at: datetime | None = None
    sunset_date: datetime | None = None

    # 검증 산출물 (참조)
    golden_eval_run_id: str | None = None
    aa_test_run_id: str | None = None

    # 검색 보조
    embedding: list[float] | None = None       # description + tags 임베딩 (검색용)

C26 AgentRecord와 거의 동일한 패턴 — 스킬도 6단계 lifecycle. 차이는 tools_required·skills_called·models_compatible 같은 의존 필드.

3 등록 API

# app/skills/registry/store.py
class SkillRegistry:
    def __init__(self, db, git_repo):
        self.db = db
        self.git = git_repo

    def register(self, skill_dir: Path, owner: str) -> SkillRecord:
        # 1. SKILL.md 파싱 + 검증
        manifest = parse_manifest(skill_dir / "SKILL.md")
        validate_schema(manifest)

        # 2. 중복 검사
        if self.db.skills.find_one(skill_id=manifest["id"], version=manifest["version"]):
            raise ValueError(f"already exists: {manifest['id']}@{manifest['version']}")

        # 3. 의존 도구 존재 확인
        for tool in manifest["tools_required"]:
            if not self.tools.exists(tool):
                raise ValueError(f"required tool not registered: {tool}")

        # 4. 호출하는 다른 스킬 존재 확인 (composition)
        for called in manifest.get("skills_called", []):
            if not self.skills.exists(called):
                raise ValueError(f"called skill not registered: {called}")

        # 5. 임베딩 생성 (검색용)
        embedding = embed(manifest["description"] + " ".join(manifest["tags"]))

        # 6. git sha 확보 + insert
        record = SkillRecord(
            **manifest,
            stage="draft",
            traffic_pct=0,
            owner=owner,
            git_path=str(skill_dir / "SKILL.md"),
            git_sha=self.git.head_sha(),
            embedding=embedding,
            created_at=datetime.utcnow(),
        )
        self.db.skills.insert(record)
        return record

등록은 C26 promote 패턴과 같음 — draft 시작, 단계별 게이트 필수.

4 검색·Discovery

4.1 이름·태그 기반

def find_by_name(self, query: str) -> list[SkillRecord]:
    """exact·prefix·fuzzy."""
    return self.db.skills.find(skill_id=query) \
        or self.db.skills.find(skill_id={"$regex": f"^{query}"}) \
        or self._fuzzy_search(query)


def find_by_tag(self, tag: str) -> list[SkillRecord]:
    return self.db.skills.find(tags=tag, stage="active")

5 버전 Resolution

같은 스킬 ID에 여러 버전이 동시 존재 (canary·staged·active). 호출 시 어느 것을 줄지 정책 필요.

class VersionResolver:
    """호출자가 명시 안 했을 때의 정책."""

    def resolve(self, skill_id: str, ctx: dict | None = None) -> SkillRecord:
        # 1. 정확 버전 명시
        if ctx and ctx.get("version"):
            return self._exact(skill_id, ctx["version"])

        # 2. 호출 사용자가 canary·staged 대상이면
        if ctx and self._user_in_experiment(ctx["user_id"], skill_id):
            return self._experimental(skill_id, ctx)

        # 3. active stage 기본
        return self._active(skill_id)

    def _active(self, skill_id) -> SkillRecord:
        rec = self.db.skills.find_one(skill_id=skill_id, stage="active")
        if not rec:
            raise NotFound(f"no active version of {skill_id}")
        return rec

    def _exact(self, skill_id, version) -> SkillRecord:
        return self.db.skills.find_one(skill_id=skill_id, version=version)

    def _experimental(self, skill_id, ctx) -> SkillRecord:
        # sticky hash로 일관 할당
        if sticky_hash(ctx["user_id"], skill_id) < canary_threshold(skill_id):
            return self.db.skills.find_one(skill_id=skill_id, stage="canary")
        return self._active(skill_id)

호출자가 버전 결정의 부담을 갖지 않음 — registry가 정책 적용.

6 의존성 그래프

[Tool] ─── (used by) ──→ [Skill] ─── (used by) ──→ [Agent]
                            ↓
                      (calls) (composition)
                            ↓
                         [Skill]

이 관계를 그래프로 저장 — 영향 분석에 핵심.

# app/skills/registry/dependencies.py
from collections import defaultdict
import networkx as nx


class DependencyGraph:
    def __init__(self):
        self.G = nx.DiGraph()

    def build(self, registry):
        for skill in registry.all_active():
            for tool in skill.tools_required:
                self.G.add_edge(tool, skill.skill_id, kind="tool")
            for sub in skill.skills_called:
                self.G.add_edge(sub, skill.skill_id, kind="skill")
        for agent in registry.all_active_agents():
            for skill in agent.skills:
                self.G.add_edge(skill, agent.agent_id, kind="agent")

    def downstream(self, node: str) -> list[str]:
        """이 노드에 의존하는 모든 상위."""
        return list(nx.descendants(self.G, node))

    def upstream(self, node: str) -> list[str]:
        """이 노드가 의존하는 모든 하위."""
        return list(nx.ancestors(self.G, node))

    def detect_cycles(self) -> list[list[str]]:
        return list(nx.simple_cycles(self.G))

6.1 영향 분석

# tool API 변경 시 영향 받는 스킬·에이전트
affected = graph.downstream("github_search")
# → ["summarize_pr", "code_review", "qna_chatbot"]

# 한 스킬 deprecate 시 영향
to_notify = graph.downstream("summarize_doc_v1")

C26 deprecate 알림이 이 그래프 사용.

6.2 Cycle Detection

스킬 A가 B를 호출하고 B가 A를 호출하면 무한 재귀. 등록 시점에 차단:

def check_no_cycle_on_register(skill_id, calls):
    test_graph = self.G.copy()
    for c in calls:
        test_graph.add_edge(c, skill_id, kind="skill")
    if list(nx.simple_cycles(test_graph)):
        raise ValueError(f"would create cycle: {skill_id}{calls}")

7 Storage 전략

7.1 Git + DB Hybrid

[Git] (단일 진실 — SKILL.md 파일)
   ↓ (commit hook)
[CI/CD]  (validation·등록)
   ↓
[DB] (조회·검색·통계)
  • Git: SKILL.md, prompt.j2, tests — 사람·도구가 직접 수정
  • CI/CD: PR 머지 시 자동 검증 + DB 동기화
  • DB: 빠른 조회·통계·embedding 인덱스
# .github/workflows/skill_register.yml
on:
  push:
    paths: ["skills/**/SKILL.md"]

jobs:
  validate_and_register:
    steps:
      - uses: actions/checkout@v4
      - run: python -m scripts.validate_skill --path skills/${{ env.SKILL_DIR }}
      - run: python -m scripts.run_golden_eval
      - run: python -m scripts.register_to_db --stage draft

이 패턴이 07-1 GitHub Actions 위에 자연스럽게 얹힘.

8 Caching 전략

레지스트리 read는 매 query마다 발생. DB 직접 조회는 비효율.

# app/skills/registry/cache.py
import redis

class CachedRegistry:
    def __init__(self, db, cache_ttl=300):
        self.db = db
        self.redis = redis.Redis()
        self.ttl = cache_ttl

    def resolve(self, skill_id: str, ctx: dict | None = None) -> SkillRecord:
        key = f"skill:{skill_id}:{self._ctx_hash(ctx)}"
        cached = self.redis.get(key)
        if cached:
            return SkillRecord.parse_raw(cached)

        record = self.resolver.resolve(skill_id, ctx)
        self.redis.setex(key, self.ttl, record.json())
        return record

8.1 Cache Invalidation

스킬 promote·deprecate 시 캐시 무효화 필수:

def on_skill_promote(skill_id: str):
    # 패턴 매칭으로 일괄 삭제
    for key in redis.scan_iter(f"skill:{skill_id}:*"):
        redis.delete(key)

또는 stale-while-revalidate — 캐시 hit 후 백그라운드로 갱신:

def resolve_swr(self, skill_id, ctx):
    cached = self.redis.get(...)
    if cached:
        # 백그라운드 갱신
        asyncio.create_task(self._refresh(skill_id, ctx))
        return cached
    return self._fetch_and_cache(skill_id, ctx)

9 CI/CD 통합 — PR Workflow

1. 개발자가 skills/new_skill/SKILL.md 추가하는 PR 생성
2. CI가 자동 실행:
   - SKILL.md schema 검증
   - tools_required 존재 확인
   - skills_called 존재 확인 + cycle 검사
   - Golden eval (C30) 실행
   - A/A test
3. 모두 통과 → PR에 자동 코멘트 (검증 결과)
4. 사람 reviewer 승인
5. 머지 → Webhook이 등록 API 호출 → DB에 draft 추가
6. 매뉴얼 또는 자동으로 promote (canary → ...)

코드와 동일 절차로 운영 — 스킬 변경의 안전성이 코드 변경 수준으로.

10 MINERVA 적용

app/skills/registry/
├── models.py               # SkillRecord schema
├── store.py                # DB 인터페이스
├── git_sync.py             # SKILL.md ↔ DB 동기화
├── search.py               # name/tag/embedding/hybrid
├── resolver.py             # 버전 정책
├── dependencies.py          # NetworkX 그래프
├── cache.py                # Redis cache + invalidation
└── audit.py                # 사용 통계·deprecated 탐지

scripts/
├── skill_register.py       # CLI 등록
├── skill_search.py          # CLI 검색
├── skill_audit.py           # 사용 빈도·고아 스킬 탐지
└── skill_graph.py           # 의존성 시각화

infra/
├── postgres                 # registry primary
├── redis                    # cache
└── pgvector                 # embedding search

C26 AgentRecord registry와 같은 인프라 — 통합 운영.

11 자주 발생하는 함정

11.1 Registry SPOF

레지스트리 down → 라우팅 결정 못 함 → 시스템 전체 down. C26 함정 동일.

해법: - read는 cache 우선 (Redis 5분 TTL) - registry down 시 last-known config로 degraded 모드 - write는 git이 primary (DB는 read replica)

11.2 Cache Invalidation

promote 후 일부 노드의 cache가 갱신 안 됨 → 옛 버전이 아직 호출됨.

해법: - promote 직후 invalidation 강제 (모든 cache 노드에 publish) - TTL을 짧게 (5분 이하) - stale-while-revalidate로 미세 차이 감수

11.3 Search Hit Miss

LLM이 의도와 다른 스킬을 찾음 — 임베딩이 description의 표현 차이에 민감.

해법: - 임베딩 + tags + name의 hybrid score - 사용자 피드백 (“이 스킬이 적합했나요?”) 수집 → embedding fine-tune - 분기마다 search 정확도 평가 (Top-1·Top-5)

11.4 Dependency Cycle

A → B → A 같은 사이클이 등록 시 차단됐어도, 별도 PR이 합쳐서 사이클이 만들어질 수 있음.

해법: - 등록 시점뿐 아니라 매 PR merge 후 graph audit - 사이클 발견 시 자동 PR comment + alert

11.5 Orphan Skills

호출되지 않는 스킬이 active 상태로 남아있음 — 코드 부채.

해법: - 분기 audit — 6개월 호출 0건이면 deprecation 검토 - 의존성 그래프에서 indegree 0인 active 스킬 자동 표시

11.6 Git ↔︎ DB Drift

git에는 새 SKILL.md, DB에는 old version. CI가 일시 실패한 후 손으로 안 맞춤.

해법: - git을 단일 진실로 — DB는 캐시·인덱스 역할 - 매일 reconciliation job (git → DB 일관성 점검) - 불일치 시 alert + 자동 재등록

11.7 Backward-incompatible 변경

tools_required에서 도구 1개 제거 → 호출 에이전트의 권한 매트릭스도 변경 필요. registry가 추적 안 하면 모름.

해법: - 의존성 graph 변경을 breaking change로 분류 (semver minor → major) - 영향 받는 모든 노드에 자동 알림 - C26 governance 게이트 강제

12 정리

영역 핵심
데이터 모델 SkillRecord — id·version·stage·dependencies·embedding
등록 git → CI 검증 → DB draft 등록 → C26 promote
검색 name·tag·embedding hybrid, semantic 위주
Resolution exact·experimental(canary)·active 기본
의존성 tool→skill→skill·agent 그래프, cycle 차단, 영향 분석
Storage git primary + DB cache, 매일 reconciliation
Cache Redis + invalidation + stale-while-revalidate
CI/CD PR 머지 → 자동 검증·등록·draft 진입
함정 SPOF·cache invalidation·search miss·cycle·orphan·drift·breaking change

13 응용 분야

시나리오 활용
LLM이 적합한 스킬 동적 선택 semantic search + hybrid score
새 도구 도입 영향 분석 dependency graph downstream
사용 안 되는 스킬 정리 orphan detection (indegree 0)
분기 운영 보고서 registry audit (사용 빈도·중복·deprecated)
스킬 variant A/B 분배 resolver의 experimental policy
외부 모델 deprecation 대응 models_compatible 필드로 영향 스킬 추출

14 관련 주제

선행 학습 (선수)

18-LangGraph 시리즈 cross-reference

  • #16 SKILL.md 명세 — 본 편 SKILL.md 표준의 origin
  • #14 스킬 설계와 생성 — 등록 워크플로 이론

후속 (Phase C-7)

Cross-reference

Subscribe

Enjoy this blog? Get notified of new posts by email: