ouroboros · 진입점 / CLI / ooo 커맨드 라우팅

한 줄 요약

ooo run 같은 한 줄 명령이 “실제로 무엇을 실행할지”를 정하는 교통정리 계층이다. 왜 배우나: 모든 기능이 이 진입점을 통과하므로, 여기를 알면 “내가 친 명령이 어디로 흘러가는지”를 끝까지 추적할 수 있다.


그림

같은 ooo 명령이 세 갈래 통로 중 하나로 흐른다. 핵심은 “AI에게 레시피를 건네는 길”과 “자판기 버튼을 누르는 길”이 갈라진다는 점.

flowchart TD
    U["사용자 명령<br/>ooo run / /ouroboros:run"]
    U --> P1["통로 1 · 선언적 스킬<br/>(AI 세션)"]
    U --> P2["통로 2 · 결정론적 라우터<br/>(런타임 어댑터)"]
    U --> P3["통로 3 · 파이썬 CLI<br/>(터미널)"]
    P1 --> S1["표 보고 SKILL.md를<br/>Read해 그대로 따름"]
    P2 --> S2["정규식 파싱 → 레지스트리<br/>→ MCP 도구 호출로 환원"]
    P3 --> S3["typer 명령 트리가<br/>run/auto/qa로 분기"]
    S1 --> R["실제 작업 실행<br/>(에이전트/MCP/오케스트레이터)"]
    S2 --> R
    S3 --> R

쉽게 풀기

식당을 떠올리자. 손님이 똑같이 “스테이크 주세요”라고 해도, 가게 구조에 따라 처리 경로가 다르다.

통로 1 — 요리사(AI)에게 레시피 카드를 건네는 길 매니저(AI)가 벽에 붙은 안내표(CLAUDE.md / AGENTS.md)를 본다. 표에는 “이 주문 → ‘run’ 레시피 카드(skills/run/SKILL.md)를 꺼내 읽고 따르라”고 적혀 있다. 즉 코드를 실행하는 게 아니라 AI에게 읽을 파일을 알려주는 매핑 표다. 카드 맨 위 메모(프론트매터)엔 “어떤 주방기구(MCP 도구)를 어떤 재료(인자)로 쓸지”까지 적혀 있다.

중요 규칙: “Skill 도구 말고 Read 도구로 파일을 직접 열어 따르라”(CLAUDE.md). AI가 딴 길로 새지 않게 못 박는 장치다.

통로 2 — 자동 분류기(결정론적 라우터) Codex·Hermes·Opencode 등 다른 런타임은 AI 자유 해석 대신 기계적 분류기를 쓴다. 정규식으로 명령 앞부분만 인식(parse_ooo_command) → 레지스트리에서 스킬 조회 → 카드의 MCP 설정을 검증·치환 → “도구 이름 + 인자”로 환원. 로그·부작용 없는 순수 함수 체인이라 예측 가능하다.

통로 3 — 자판기(파이썬 CLI) AI 없이 터미널에서 ouroboros run seed.yaml을 직접 칠 수도 있다. typer가 만든 명령 버튼 트리(main.py)가 run/auto/qa 같은 파이썬 함수로 분기하고, 등록 안 된 이름은 “플러그인 디스패치” 예비 버튼으로 떨어진다.

입구는 모두 ooo지만 처리 엔진이 다르다. 학생이 외울 핵심은 두 장의 표 — “명령 → 스킬 파일” 표“명령 → CLI 핸들러” 표.

통로 2(결정론적 라우터)의 내부 단계 흐름은 다음과 같다.

flowchart TD
    A["프롬프트<br/>ooo run seed.yaml"] --> B["parse_ooo_command<br/>정규식 → ParsedOooCommand"]
    B -->|매치 실패| Z["NotHandled<br/>(스킬 명령 아님)"]
    B --> C["registry.resolve<br/>skill_name → 대상<br/>(디렉터리명/name/alias)"]
    C -->|없음| Y["NotHandled<br/>SKILL_NOT_FOUND"]
    C --> D["프론트매터 로드<br/>mcp_tool / mcp_args"]
    D --> E["검증·치환<br/>$1/$CWD/$args"]
    E --> G["Resolved<br/>(mcp_tool, mcp_args)"]
    G --> H["런타임 어댑터가<br/>MCP 핸들러 호출"]

핵심 정리

통로입구실제로 하는 일
선언적 스킬AI 채팅 ooo X표 보고 SKILL.md를 Read해 따름
결정론적 라우터런타임 어댑터정규식→레지스트리→MCP 호출로 환원
파이썬 CLI터미널 ouroboros Xtyper가 파이썬 핸들러 실행

실제 예시

선언적 라우팅의 세 파일이 어떻게 연결되는지 먼저 본다.

flowchart LR
    M["CLAUDE.md/AGENTS.md<br/>매핑 표"] -->|가리킴| SK["skills/run/SKILL.md<br/>프론트매터=MCP 계약"]
    C["commands/run.md<br/>슬래시 포워더"] -->|Read 지시| SK
    SK -->|"mcp_tool+mcp_args"| MCP["MCP 핸들러 호출"]

1) 선언적 라우팅 표 — AI가 읽는 매핑 (CLAUDE.md)

## ooo Commands (Dev Mode)
When the user types any of these commands, read the corresponding
SKILL.md file and follow its instructions exactly:
 
| Input | Action |
|-------|--------|
| `ooo` (bare) | Read `skills/welcome/SKILL.md` and follow it |
| `ooo run` | Read `skills/run/SKILL.md` and follow it |
| `ooo evaluate` or `ooo eval` | Read `skills/evaluate/SKILL.md` and follow it |
**Important**: Do NOT use the Skill tool. Read the file with the Read tool.

2) SKILL.md 프론트매터(=MCP 디스패치 계약)

# skills/run/SKILL.md
---
name: run
description: "Execute a Seed specification through the workflow engine"
mcp_tool: ouroboros_execute_seed
mcp_args:
  seed_path: "$1"     # remainder 첫 인자(=전체 페이로드)
  cwd: "$CWD"         # 런타임 cwd
---

3) 레지스트리 — 식별자 우선순위(디렉터리명 > name/alias)

# src/ouroboros/router/registry.py
def _build_identifier_mapping(targets):
    mapping = {}
    for target in targets:               # 1순위: 디렉터리명
        mapping[target.skill_name] = target
    for target in targets:               # 2순위: name/alias (덮어쓰지 않음)
        for identifier in target.identifiers:
            mapping.setdefault(identifier, target)
    return mapping

CLI 진입점은 typer 그룹이 1순위로 등록 명령을, 못 찾으면 플러그인 디스패치로 떨어지는 2단 구조다.

flowchart TD
    IN["ouroboros mycmd"] --> G["_PluginAwareGroup.get_command"]
    G --> Q{"등록된<br/>first-party 명령?"}
    Q -->|있음| F["해당 핸들러 실행"]
    Q -->|없음| PL["build_plugin_dispatch_command<br/>(플러그인 fallback)"]
    F --> SUB{"run 서브커맨드<br/>매치?"}
    SUB -->|"아니오·옵션 아님"| WF["workflow로 폴백<br/>run seed.yaml = run workflow seed.yaml"]

직접 만들 때 — 새 명령 추가 4단계

# 2) skills/mycmd/SKILL.md (1번은 CLAUDE.md/AGENTS.md 표에 행 추가)
---
name: mycmd
description: "한 줄 설명"
aliases: [mc]                    # registry가 식별자로 등록
mcp_tool: ouroboros_my_handler   # 하이픈 금지
mcp_args:
  goal: "$args"                  # 옵션 제거된 위치 인자
  cwd: "$CWD"
---

요약 & 셀프체크

3줄 요약:

  1. ooo 명령은 세 통로(선언적 스킬 · 결정론적 라우터 · 파이썬 CLI) 중 하나로 흐르며, 입구는 같아도 처리 엔진이 다르다.
  2. AI 세션에서는 CLAUDE.md/AGENTS.md 표가 라우터가 되어 SKILL.md를 Read해 따르고, 카드의 mcp_tool/mcp_args가 MCP 호출 계약이 된다.
  3. 터미널에서는 typer app이 명령을 분기하며, 미등록 이름은 플러그인 fallback, run seed.yamlrun workflow seed.yaml로 폴백된다.

스스로 답해보기:

  • ooo run을 채팅에 쳤을 때와 터미널에 쳤을 때, 각각 어떤 통로를 타고 무엇이 다른가?
  • 같은 명령에 디렉터리명과 alias가 충돌하면 어느 쪽이 이기고, 그 이유는?
  • SKILL.md 프론트매터에서 $1$args를 잘못 바꿔 쓰면 어떤 인자가 잘못 전달될까?

연결

OB_개요 · _분석축_루브릭