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 X
typer가 파이썬 핸들러 실행
펼쳐보기: 외워야 할 핵심 스키마 4종 + 헷갈리는 규칙
스키마 4종
선언적 라우팅 표(CLAUDE.md/AGENTS.md): Input(명령 문자열) → Action(Read skills/X/SKILL.md and follow it). 이 표 자체가 라우터다.
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 correspondingSKILL.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: rundescription: "Execute a Seed specification through the workflow engine"mcp_tool: ouroboros_execute_seedmcp_args: seed_path: "$1" # remainder 첫 인자(=전체 페이로드) cwd: "$CWD" # 런타임 cwd---
펼쳐보기: 슬래시 커맨드 포워더 전문 (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.
펼쳐보기: 결정론적 파서 코드 전문 (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.pydef _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"]
펼쳐보기: CLI 진입점 + 두 가지 fallback 코드 전문
# src/ouroboros/cli/main.py — 미등록 이름 플러그인 fallbackclass _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)
// 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.pyfrom ouroboros.cli.commands import mycmdapp.add_typer(mycmd.app, name="mycmd") # 서브커맨드 그룹# 또는 단일 명령:app.command(name="mycmd")(mycmd.mycmd_command)
펼쳐보기: 추가 시 체크리스트 전체
CLAUDE.md 와 AGENTS.md 양쪽 표에 명령 추가 (Claude/Codex 양쪽 지원)
skills/<name>/SKILL.md에 name + mcp_tool + mcp_args 필수 (없으면 InvalidSkill)
mcp_tool은 하이픈 금지, 식별자/별칭은 소문자 규칙 준수
디렉터리명이 1순위 — alias는 setdefault라 디렉터리명을 못 덮음
$1(전체 remainder) vs $args/$goal(옵션 제거 위치 인자) 의미 구분