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 X | typer가 파이썬 핸들러 실행 |
[!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.md에name+mcp_tool+mcp_args필수 (없으면InvalidSkill)mcp_tool은 하이픈 금지, 식별자/별칭은 소문자 규칙 준수- 디렉터리명이 1순위 — alias는
setdefault라 디렉터리명을 못 덮음$1(전체 remainder) vs$args/$goal(옵션 제거 위치 인자) 의미 구분- typer 미등록 이름은 자동으로 플러그인 디스패치로 떨어짐 — 이름 충돌 주의
- CLI 핸들러는 EventStore/SessionRepository 계약(이벤트소싱) 준수
요약 & 셀프체크
3줄 요약:
ooo명령은 세 통로(선언적 스킬 · 결정론적 라우터 · 파이썬 CLI) 중 하나로 흐르며, 입구는 같아도 처리 엔진이 다르다.- AI 세션에서는 CLAUDE.md/AGENTS.md 표가 라우터가 되어 SKILL.md를 Read해 따르고, 카드의
mcp_tool/mcp_args가 MCP 호출 계약이 된다. - 터미널에서는 typer
app이 명령을 분기하며, 미등록 이름은 플러그인 fallback,run seed.yaml은run workflow seed.yaml로 폴백된다.
스스로 답해보기:
ooo run을 채팅에 쳤을 때와 터미널에 쳤을 때, 각각 어떤 통로를 타고 무엇이 다른가?- 같은 명령에 디렉터리명과 alias가 충돌하면 어느 쪽이 이기고, 그 이유는?
- SKILL.md 프론트매터에서
$1과$args를 잘못 바꿔 쓰면 어떤 인자가 잘못 전달될까?
연결
[!note]- 펼쳐보기: 근거 파일 전체 목록
CLAUDE.md— 선언적 명령 표, “Read SKILL.md and follow”, Skill 도구 금지 규칙, on-demand 로드 표AGENTS.md— Codex용 동일 표 (ouroboros_start_auto등 일부 도구명 차이)src/ouroboros/cli/main.py— typerapp,_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_commandsrc/ouroboros/router/registry.py—SkillDispatchRegistry,_build_identifier_mapping, alias 필드,normalize_skill_identifiersrc/ouroboros/router/dispatch.py—resolve_skill_dispatch,normalize_mcp_frontmatter, 템플릿 치환($1/$CWD/$args/$goal)src/ouroboros/router/types.py—ParsedOooCommand,Resolved,NotHandled,InvalidSkill, MCP 디스패치 타입commands/run.md— 슬래시 커맨드 → SKILL.md 포워더skills/run/SKILL.md— 실제mcp_tool: ouroboros_execute_seed/mcp_args예시hooks/hooks.json—UserPromptSubmit(keyword-detector),SessionStart,PostToolUse(drift-monitor) 훅