지식위키

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

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가 파이썬 핸들러 실행

[!note]- 펼쳐보기: 외워야 할 핵심 스키마 4종 + 헷갈리는 규칙 스키마 4종

  • 선언적 라우팅 표(CLAUDE.md/AGENTS.md): Input(명령 문자열) → Action(Read skills/X/SKILL.md and follow it). 이 표 자체가 라우터다.
  • SKILL.md 프론트매터: name(필수), mcp_tool(필수, 정규식 ^[A-Za-z_][A-Za-z0-9_]*$), mcp_args(필수), aliases(선택). MCP 디스패치 계약.
  • ParsedOooCommand(파서 출력): skill_name(소문자 정규화), command_prefix(ooo {skill} 또는 /ouroboros:{skill}), remainder(뒤 인자 텍스트, 멀티라인 보존).
  • 파이썬 CLI 명령 트리: auto/run/qa는 직접 등록, 미등록 이름은 _PluginAwareGroup이 플러그인 fallback으로 처리.

헷갈리는 규칙

  • 디렉터리명이 1순위 식별자. alias는 setdefault로 등록되어 디렉터리명을 못 덮는다.
  • 식별자/별칭 소문자 규칙 ^[a-z0-9][a-z0-9_-]*$ (안 맞으면 normalize_skill_identifier가 None).
  • mcp_tool은 하이픈 금지(^[A-Za-z_][A-Za-z0-9_]*$).
  • $1(전체 remainder) ≠ $args/$goal(옵션 제거된 위치 인자).

실제 예시

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

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
---

[!note]- 펼쳐보기: 슬래시 커맨드 포워더 전문 (commands/run.md)

// /home/seunghyeong/harness-work/ouroboros/commands/run.md
---
description: "Execute a Seed specification through the workflow engine"
aliases: [execute]
---
Read the file at `${CLAUDE_PLUGIN_ROOT}/skills/run/SKILL.md`
using the Read tool and follow its instructions exactly.

[!note]- 펼쳐보기: 결정론적 파서 코드 전문 (command_parser.py)

# /home/seunghyeong/harness-work/ouroboros/src/ouroboros/router/command_parser.py
_SKILL_COMMAND_PREFIX_PATTERN = re.compile(
    r"^\s*(?:(?P<ooo_prefix>ooo)\s+(?P<ooo_skill>[a-z0-9][a-z0-9_-]*)|"
    r"(?P<slash_prefix>/ouroboros:)(?P<slash_skill>[a-z0-9][a-z0-9_-]*))",
    re.IGNORECASE,
)

def parse_ooo_command(prompt: str) -> ParsedOooCommand | None:
    match = _SKILL_COMMAND_PREFIX_PATTERN.match(prompt)
    if match is None:
        return None
    skill_name = (match.group("ooo_skill") or match.group("slash_skill") or "").lower()
    ...
    command_prefix = (
        f"ooo {skill_name}" if match.group("ooo_skill") is not None
        else f"/ouroboros:{skill_name}"
    )
    return ParsedOooCommand(skill_name=skill_name,
                            command_prefix=command_prefix, remainder=remainder)

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"]

[!note]- 펼쳐보기: CLI 진입점 + 두 가지 fallback 코드 전문

# src/ouroboros/cli/main.py — 미등록 이름 플러그인 fallback
class _PluginAwareGroup(TyperGroup):
    def get_command(self, ctx, cmd_name):
        cmd = super().get_command(ctx, cmd_name)   # 1순위: 등록된 first-party
        if cmd is not None:
            return cmd
        return build_plugin_dispatch_command(cmd_name)  # 없으면 플러그인 디스패치

app = typer.Typer(name="ouroboros", no_args_is_help=True, cls=_PluginAwareGroup)
app.command(name="auto", ...)(auto.auto_command)
app.add_typer(run.app, name="run")
app.command(name="qa", ...)(qa.qa_command)
app.add_typer(mcp.app, name="mcp")
# src/ouroboros/cli/commands/run.py — 서브커맨드 생략 단축형
class _DefaultWorkflowGroup(typer.core.TyperGroup):
    """안 맞으면 'workflow'로 폴백 → `run seed.yaml` == `run workflow seed.yaml`"""
    default_cmd_name: str = "workflow"
    def parse_args(self, ctx, args):
        if args and args[0] not in self.commands and not args[0].startswith("-"):
            args = [self.default_cmd_name, *args]
        return super().parse_args(ctx, args)

직접 만들 때 — 새 명령 추가 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"
---

[!note]- 펼쳐보기: 1·3·4단계 코드 전문 (표 행 / 포워더 / CLI 등록)

// 1) 선언적 매핑 표 (CLAUDE.md / AGENTS.md)
| `ooo mycmd` or `ooo mc` | Read `skills/mycmd/SKILL.md` and follow it |
// 3) 슬래시 커맨드 포워더 commands/mycmd.md
---
description: "한 줄 설명"
aliases: [mc]
---
Read the file at `${CLAUDE_PLUGIN_ROOT}/skills/mycmd/SKILL.md` using the Read tool and follow its instructions exactly.
# 4) 파이썬 CLI 핸들러 등록 cli/main.py
from ouroboros.cli.commands import mycmd
app.add_typer(mycmd.app, name="mycmd")     # 서브커맨드 그룹
# 또는 단일 명령:
app.command(name="mycmd")(mycmd.mycmd_command)

[!note]- 펼쳐보기: 추가 시 체크리스트 전체

  • CLAUDE.md AGENTS.md 양쪽 표에 명령 추가 (Claude/Codex 양쪽 지원)
  • skills/<name>/SKILL.mdname + mcp_tool + mcp_args 필수 (없으면 InvalidSkill)
  • mcp_tool은 하이픈 금지, 식별자/별칭은 소문자 규칙 준수
  • 디렉터리명이 1순위 — alias는 setdefault라 디렉터리명을 못 덮음
  • $1(전체 remainder) vs $args/$goal(옵션 제거 위치 인자) 의미 구분
  • typer 미등록 이름은 자동으로 플러그인 디스패치로 떨어짐 — 이름 충돌 주의
  • CLI 핸들러는 EventStore/SessionRepository 계약(이벤트소싱) 준수

요약 & 셀프체크

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_개요 · _분석축_루브릭

[!note]- 펼쳐보기: 근거 파일 전체 목록

  • CLAUDE.md — 선언적 명령 표, “Read SKILL.md and follow”, Skill 도구 금지 규칙, on-demand 로드 표
  • AGENTS.md — Codex용 동일 표 (ouroboros_start_auto 등 일부 도구명 차이)
  • src/ouroboros/cli/main.py — typer app, _PluginAwareGroup 플러그인 fallback, add_typer/command 등록
  • src/ouroboros/cli/commands/run.py_DefaultWorkflowGroup 단축형, workflow 핸들러, _run_orchestrator(EventStore/QA)
  • src/ouroboros/router/command_parser.py_SKILL_COMMAND_PREFIX_PATTERN, parse_ooo_command
  • src/ouroboros/router/registry.pySkillDispatchRegistry, _build_identifier_mapping, alias 필드, normalize_skill_identifier
  • src/ouroboros/router/dispatch.pyresolve_skill_dispatch, normalize_mcp_frontmatter, 템플릿 치환($1/$CWD/$args/$goal)
  • src/ouroboros/router/types.pyParsedOooCommand, Resolved, NotHandled, InvalidSkill, MCP 디스패치 타입
  • commands/run.md — 슬래시 커맨드 → SKILL.md 포워더
  • skills/run/SKILL.md — 실제 mcp_tool: ouroboros_execute_seed / mcp_args 예시
  • hooks/hooks.jsonUserPromptSubmit(keyword-detector), SessionStart, PostToolUse(drift-monitor) 훅