개요
OpenArms에 기여하려는 개발자를 위한 장입니다. OpenArms는 하드코딩된 Hermes/AGY 런타임이 아니라 역할과 얇은 프로바이더 부착을 중심으로 자랍니다.
아키텍처 경계
PM 역할 → 프로바이더 중립 판단·수명주기·보드·정리 정책
워커 역할 → 프로바이더 중립 증거·결과 제출
프로바이더 → 그 역할을 현재 맡은 교체 가능한 CLI·메신저 구현핵심 불변식은 전경 세션 권위, supersede-not-delete(이력 보존), 그리고 표면에 날 내부를 드러내지 않는 것입니다.
엔진은 openarms/ 아래의 중립 코어이고, 각 프로바이더는 integrations/<provider>/
아래의 교체 가능한 어댑터입니다. 코어는 PM·워커·라우팅·계약 같은 역할 용어로만
말하고, 한 CLI의 pane 관용구나 키워드 휴리스틱을 박아 넣지 않습니다. 그래서 hermes=PM,
antigravity=worker는 현재 라이브 바인딩의 인스턴스이지 제품 기본값이 아닙니다.
계약 모델
모든 부착은 공식 CLI 계약으로 표현됩니다 — 권위 있는 고용/계약 세션, 입력 전달, 출력 포착, 터미널 잉크 제거, 계약을 닫는 증거.
이 챕터가 다루는 서비스·도구
아래는 기여자가 실제로 손대는 스택입니다. 각 항목마다 왜 골랐는지 / 어떻게 구성하는지 / 배포판에서 훅을 어떻게 쓰는지를 구체적으로 답합니다.
tmux — 라이브 전경 실행석(execution seat) backend
왜 이걸 선택했는지. PM·워커 역할을 맡은 CLI(Hermes/AGY/Claude)는 사람이 쓰는 것과
같은 구독 권위의 전경 세션 안에서 돌아야 합니다. 그래야 PM 두뇌가 과금되는 headless
호출이 아니라, 사용자 좌석과 동일한 권위를 갖는 살아있는 세션이 됩니다. tmux는 이 좌석을
프로세스로부터 분리(detach)해 붙잡아 두면서, 외부에서 send-keys로 입력을 주입하고
capture-pane으로 화면을 읽을 수 있는 가장 단순한 backend입니다. pty를 직접 다루거나
화면 스크레이퍼를 짜는 것보다 의존성이 가볍고, WSL/Linux/macOS/Mac VM에서 자연스럽게
동작합니다. 단, tmux는 제품 정체성이 아니라 현재 구현의 backend입니다 — 로드맵은
execution_seat를 tmux/pty/ssh/windows-terminal/provider-native로 추상화하는 방향입니다.
Windows 네이티브는 tmux가 기본 환경이 아니므로 비주력(개발·문서·Telegram GUI 확인 쪽)입니다.
어떻게 구성하는지. 좌석은 명명된 세션·윈도·pane으로 잡습니다. 주입 대상은
<session>:<window>.<pane> 형식의 타깃 문자열입니다(예: fleet:openarms-pm.0). 환경변수로
바인딩합니다.
# PM 좌석 pane을 띄우고(예: fleet 세션의 openarms-pm 윈도) 거기서 PM CLI를 실행해 둔다
tmux new-session -d -s fleet -n openarms-pm
export OPENARMS_PM_PANE="fleet:openarms-pm.0" # 주입 타깃
export OPENARMS_TMUX="tmux" # tmux 바이너리send-keys 패턴(중요). 텍스트와 Enter를 반드시 분리해서 보냅니다. 본문은 -l로
리터럴 전송하고(키 이름 해석 끄기), Enter는 별도 호출로 키로 보냅니다. 그래야 본문 속
“Enter” 같은 단어가 키로 오해되지 않고, 줄바꿈이 정확히 한 번만 들어갑니다. 실제 주입기는
이 패턴을 그대로 씁니다.
# integrations/claude_code/openarms_provider/inject_listener.py 의 핵심
subprocess.run([TMUX, "send-keys", "-t", PANE, "-l", text], check=False) # 리터럴 본문
subprocess.run([TMUX, "send-keys", "-t", PANE, "Enter"], check=False) # 키로서의 Enter이는 tmux 커뮤니티의 권장 패턴과 일치합니다: -l은 인자를 키 이름으로 찾지 않고 UTF-8
문자 그대로 보내므로, 특수 키(Enter)는 -l 없이 별도 인자/호출로 분리해야 합니다.
Telegram Bot API (getUpdates 롱폴링) vs telethon (MTProto)
OpenArms는 두 Telegram 접근을 역할별로 다르게 씁니다. 이 구분이 기여자가 가장 많이 헷갈리는 지점입니다.
왜 이렇게 나눴는지.
- inbound 주입기·아웃바운드 send 래퍼는 봇 토큰 + HTTP
getUpdates/sendMessage만 씁니다. 외부 의존성 0, stdliburllib만으로 동작 — 좌석 호스트에 무엇이 깔렸든 굴러갑니다. 봇 API는 토큰 인증이 단순하고 실시간 업데이트에 충분합니다. 단 같은 토큰으로 동시getUpdates를 두 프로세스가 호출하면 텔레그램이 409 Conflict로 한쪽을 떨굽니다 → 주입기는 단일 인스턴스여야 합니다(런처가 기존 프로세스를pkill후 재기동하는 이유). - **telethon(MTProto)**은 메시지 히스토리 접근, 토픽/스레드 수명주기 제어, 첨부 처리 같은
봇 API로는 닿지 않는 표면이 필요할 때 씁니다. MTProto는 HTTP/폴링/웹훅 없이 텔레그램
서버에 직접 붙어 오버헤드가 작고, 봇 API에 없는 풀 표면을 줍니다(대신 세션 관리가 필요).
그래서
telethon은 테스트 필수 의존성이며(messaging/·scripts/전반 import), Hermes 플러그인 브리지의pip_dependencies에도telethon>=1.36으로 고정돼 있습니다.
어떻게 구성하는지. 봇 토큰은 owner-local secret으로, 레포에 커밋하지 않고
~/.openarms/credentials/telegram.env에 0600으로 둡니다. 주입기는 부팅 시 백로그를 건너뛰려
getUpdates?offset=-1로 현재 최대 update_id+1을 offset 파일에 박아 둔 뒤 롱폴링을 시작합니다.
# setup_pm_seat.sh — 토큰을 자격 디렉터리로 이동(0600), 소스는 삭제
sed -E 's/^export //' "$TOK_SRC" > ~/.openarms/credentials/telegram.env
chmod 600 ~/.openarms/credentials/telegram.envvenv / pip editable — 코어 패키지 개발·테스트 환경
왜 이걸 선택했는지. OpenArms 코어는 평범한 Python 패키지이고, 권위는
pyproject.toml/requirements-dev.txt입니다. 표준 [venv](https://docs.python.org/3/library/venv.html) + pip install -e .[dev]는
추가 툴 설치 없이 어디서나 동작하는 가장 보수적인 경로라 기여 진입장벽이 낮습니다. 편집
가능(editable) 설치라 소스를 고치면 재설치 없이 즉시 반영됩니다. 속도가 중요하면 [uv](https://docs.astral.sh/uv/)로
같은 일을 ~200배 빠르게 할 수 있습니다(uv venv && uv pip install -e .); -e 플래그
의미는 동일합니다. 다만 보수적 호환이 우선이라 문서 기본은 venv입니다.
어떻게 구성하는지.
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
PYTHONIOENCODING=utf-8 python3 -m pytest -q # 한글 리터럴 출력 깨짐 방지테스트는 **WSL/Linux/macOS의 python3**로 레포 루트에서 돌립니다. Windows의 hermes-agent
venv는 지원 러너가 아닙니다(거기엔 pytest/telethon이 없어 수집 자체가 실패).
의존성은 3계층 — telethon/[jsonschema](https://json-schema.org/)는 필수, [networkx](https://networkx.org/)/pyyaml은 권장(없으면 일부
테스트가 실패가 아니라 조용히 SKIP), leidenalg+python-igraph/래스터라이저는 선택입니다.
돌리려는 그 인터프리터로 먼저 점검하세요.
python3 scripts/dev_preflight.py # import-only PASS/MISSING 검사, 필수 누락 시 비-0 종료아티팩트 트리(~/.openarms)와 단일 경로 해석기 — openarms/paths.py
왜 이걸 선택했는지. 모든 영속 쓰기가 한 곳(paths.py)을 통과하므로, 관리 아티팩트
트리가 관례가 아니라 강제됩니다 — /tmp·레포 루트·인라인 .bak 더미로의 누출이
구조적으로 불가능해집니다(쓸 데가 거기밖에 없어서). stdlib만 쓰므로
python openarms/paths.py bootstrap으로 의존성 없이 골격을 만듭니다.
어떻게 구성하는지. 루트는 $OPENARMS_HOME(없으면 ~/.openarms). 주소공간은 둘:
GLOBAL 클래스 디렉터리(credentials·wiki·ledgers·logs·evidence·backups 등)와
per-seat 격리 디렉터리(seats/<id>/{state,memory,wiki}). 좌석 호스트 부트스트랩:
python3 openarms/paths.py bootstrap # config/credentials/wiki/... 골격 idempotent 생성Hermes — 현재 PM 역할 프로바이더
왜 이걸 선택했는지. Hermes CLI는 두 파일럿 프로바이더 중 하나로, 라이브 테스트베드에서 PM(조율자) 역할에 바인딩됩니다. 결정적 이유는 Hermes가 공식 플러그인 로더와 훅 계약을 제공한다는 점 — 코어를 패치하지 않고 OpenArms로 라우팅을 끼워 넣을 수 있습니다. 이 바인딩은 라이브 배포의 인스턴스일 뿐 제품 기본값이 아닙니다(역할 ≠ 프로바이더).
어떻게 구성하는지. 플러그인 브리지는 integrations/hermes/openarms_contract_bridge/에
있고, plugin.yaml 매니페스트가 의존성·필수 환경·훅 목록을 선언합니다. 환경 표면은
OPENARMS_HERMES_* 키군(바이너리·tmux pane·CLI 미러·라우팅/계약). 중립 후계 키는
OPENARMS_* 역할-우선 키로 옮겨가는 중입니다.
# integrations/hermes/openarms_contract_bridge/plugin.yaml
name: openarms-contract-bridge
pip_dependencies: [telethon>=1.36]
requires_env: [OPENARMS_ROOT, OPENARMS_HERMES_ROUTE_TARGET, OPENARMS_HERMES_ROUTE_EXECUTOR, OPENARMS_TMUX_BIN]
hooks: [pre_gateway_dispatch, transform_llm_output, cli_input_before_submit, cli_response_after_render]배포판에서 훅을 어떻게 쓰는지. Hermes의 진짜 플러그인 로더가 이 매니페스트를 읽고
훅을 OpenArms 브리지로 라우팅합니다 — Hermes 코어 패치 0. 네 훅이 게이트웨이 디스패치
전, LLM 출력 변환, CLI 제출 전, 렌더 후를 가로챕니다. 예를 들어
cli_response_after_render는 사용자에게 나가는 텍스트에 역할 헤더를 강제로 붙입니다.
# integrations/hermes/openarms_contract_bridge/gateway_hooks.py (요지)
def _format_cli_response_mirror(text: str) -> str:
return ensure_visible_role_header(
text.strip(),
provider_id=os.getenv("OPENARMS_HERMES_PROVIDER_ID", "hermes-codex-spark"),
role="employer",
provider_headers=("[PM]", "[Worker]", "[Hermes]", "[AGY]"),
)릴리스에서는 config/openarms.hermes.env.example을 런타임 환경 경계로 복사·채우고,
필요하면 검증된 패치 아티팩트(patches/hermes/...-foreground-cli-hooks.patch)를
참조합니다.
AGY (Antigravity) — 현재 worker 역할 프로바이더
왜 이걸 선택했는지. Antigravity CLI는 다른 파일럿 프로바이더로, 워커 역할에 바인딩됩니다. AGY는 네이티브 플러그인 번들(plugin/skill/commands/hooks)을 갖고 있어 워커 인입/결과 제출 경로를 자기 affordance로 표현할 수 있습니다. 역시 인스턴스 바인딩이지 기본값이 아닙니다.
어떻게 구성하는지. 번들은 integrations/antigravity/openarms_provider/에 있고
(plugin.json, commands/openarms-worker.md, hooks/openarms.json,
skills/openarms-worker/SKILL.md), 페르소나는 agy-global-AGENTS.md. 환경은
OPENARMS_AGY_* 키군이며 중립 후계는 OPENARMS_WORKER_*. 주의: OPENARMS_AGY_TELEGRAM_CHAT_ID는
AGY 복구에 필요한 fail-closed 게이트 키입니다(없으면 워커가 닫힌 채로 뜸).
배포판에서 훅을 어떻게 쓰는지. AGY는 훅을 선언형 JSON으로 등록합니다. 이 매니페스트가 워커 역할·프로바이더·권위·첨부 정책을 브리지에 알려, 네이티브 첨부 표면을 폴백보다 먼저 쓰게 합니다.
// integrations/antigravity/openarms_provider/hooks/openarms.json
{
"openarms_provider_bridge": {
"role": "worker",
"provider": "antigravity",
"authority": "official_antigravity_cli_surface",
"attachment_policy": "use_native_attachment_surface_before_fallback"
}
}PM 입출력 훅 레이어 — 주입기 + send 래퍼
왜 이렇게 나눴는지. 메커니즘과 판단을 분리하기 위해서입니다. 페르소나(스킬)는 자유롭게 말하고, 코드 백스톱이 누출·중복·재시도를 보이지 않게 강제합니다.
- inbound:
inject_listener.py는 텔레그램 텍스트를[TG chat=… from=…] <본문>봉투로 감싸 PM pane에 주입합니다. 답을 스크레이프하지 않습니다(순수 인바운드 전송) — PM 두뇌가 자기 Bash 도구로 직접 회신합니다. 그래서 브레인 종속 파싱이 없고 어떤 좌석에도 붙습니다. - outbound: PM은 raw curl 대신
pm_send.py를 호출합니다. 이 래퍼가 내부 라우팅 id/경로/봉투 마커를 정규식으로 제거(redact)하고, 90초 창 안에서 중복을 dedup하고, 실패 시 한 번 재시도한 뒤 보냅니다.
pm_send.py --chat <chat_id> [--thread <thread_id>] --text "<회신>" # 또는 --text - 로 stdin이 레이어 덕에 redaction/dedup/retry가 행동 규칙이 아니라 코드로 보장됩니다(보이지 않고, 강제됨). 판단은 스킬에, 메커니즘은 여기에.
cron — RAG 칸반 하트비트·워치독
왜 이걸 선택했는지. PM 루프를 한 틱씩 전진시키는 주기 구동에는 OS 표준 cron이 가장
단순합니다. 핵심 규율: 하트비트는 nudge를 텔레그램 메시지로 보내고 워커는 OpenArms
ingress를 폴링해서만 받습니다 — 워커 pane에 send-keys로 직접 꽂는 것은 가짜 시나리오
오염이라 금지입니다. 정지는 touch ~/.openarms/tmp/rag_kanban.stop.
어떻게 구성하는지. crontab 라인이 %를 특수문자로 다루므로 래퍼 스크립트로 감쌉니다.
래퍼는 OPENARMS_HOME과 레포 cwd를 고정해, cron의 빈 환경에서도 워커가 당기는 동일한
라이브 아카이브를 가리키게 합니다.
# rag_kanban_cron.sh — 한 번 호출 = 한 틱
export OPENARMS_HOME=~/.openarms
cd /path/to/openarms || exit 1
/usr/bin/python3 integrations/claude_code/openarms_provider/rag_kanban_loop.py \
>> "$OPENARMS_HOME/logs/rag_kanban_cron.log" 2>&1마이크로 버전 증거 규율
변경은 v0.0.0.x 슬라이스로 닫히며, 각 슬라이스는 버전·커밋·주장 수준·변경 파일·검증·
라이브 결과·새 블로커·다음 버전·정지 결정을 남깁니다. 정지 결정은 continue·hold·rollback·
stop_and_report 중 하나입니다.
기여 절차 요약
python -m venv .venv && pip install -e .[dev]로 환경을 만들고python3 scripts/dev_preflight.py로 필수 의존성을 확인한다.- 코어는
openarms/아래 중립으로, 프로바이더 특이 코드는integrations/<provider>/아래로 넣는다. 한 CLI의 mechanics를 코어에 박지 않는다. - 모든 영속 쓰기는
openarms/paths.py를 통해 해석한다(직접/tmp·레포 루트 쓰기 금지). PYTHONIOENCODING=utf-8 python3 -m pytest -q로 검증하고, 변경을 마이크로 버전 슬라이스로 닫으며 정지 결정을 남긴다.