콘텐츠로 이동

사용 설명서

원천 · framework/evomind/cli/evo기준 · 릴리스 0.23.1커밋 · 7e210096파일 · docs/cli.md기준선 · 2026-11-12

evo는 단일 core engine 위에 두 개의 프론트엔드를 둔다(Final Report §4.1):

  • TUI (기본): evo — 인터랙티브 터미널 UI
  • headless (CI/스크립트): evo -p "prompt" — stdio 프론트엔드. TUI와 동일한 engine·이벤트 스키마·지침 파일 로딩을 공유한다.

입력줄에 /를 치면 목록이 뜬다. /help는 아래 명령 전체와 키를 보여 준다. 순서는 /help와 같다.

명령 하는 일
/help 슬래시 명령과 키 목록
/goal 자율 목표 루프: set <목표> · status · pause · resume · clear (Goal 자율 루프)
/clear 새 세션 시작
/compact [지시] 대화를 요약해 새 세션으로 이어 감
/context 문맥 사용량 내역
/config 설정 목록(↑/↓ 이동, Enter/Space 변경, 바로 저장). /config show는 읽기 전용 요약
/update evo 업데이트(/update check는 확인만). 끝나면 새 버전으로 다시 시작
/cost · /usage 세션 비용·토큰 합계(/usage는 /cost의 별칭)
/doctor 환경 점검
/mcp MCP 서버와 도구
/inbox 이 세션이 받은 evo-dispatch 요청: 골라서 할 일을 말함
/dispatch evo-dispatch: name [<이름> | -] 이 세션 이름 보기·바꾸기 · list 내 활성 세션 · send 골라서 메시지 보내기
/memory 읽은 지침 파일 목록
/model 모델 보기·바꾸기
/provider 프로바이더: 안내 설정(인자 없음) · add · use · key
/login · /logout evo 로그인·로그아웃
/permissions 권한 모드 보기·바꾸기
/resume 이어 할 세션 고르기(또는 /resume <id>)
/status 세션과 환경 상태
/agents 하위 에이전트 정의
/hooks 설정된 훅 이벤트
/version evo 버전
/vim vim 모드 켜기·끄기(on/off), ~/.evo/settings.json에 저장
/mouse 마우스 사용 켜기·끄기(on/off), ~/.evo/settings.json에 저장
/exit · /quit evo 끝내기(/quit는 /exit의 별칭)

/vim으로 토글 (또는 /vim on · /vim off). 토글은 ~/.evo/settings.json의 "vimMode" 키로 영구 저장되고 다음 시작 시 자동 적용된다. 상태줄 우측에 -- (normal) / -- INSERT -- (insert) 표시.

단일 라인 버퍼에 적용되는 vim 모드 (멀티라인 전용 명령 j/k, o/O, gg/G, visual 등은 미지원).

키 동작
i · a cursor 위치 / 뒤에서 insert 모드 진입
I · A 줄 시작 / 줄 끝에서 insert 모드 진입
Esc insert → normal (cursor 한 칸 좌측 이동)
h · l (또는 Space) 한 칸 좌 / 우 (count 지원)
0 · ^ 줄 시작 / 첫 비공백
$ 줄 끝
w · b · e 다음 단어 시작 / 이전 단어 시작 / 단어 끝 (count 지원)
f · F · t · T + 대상 문자 해당 문자로 이동 (t/T는 인접 문자)
; · , 마지막 find 반복 / 반대 방향 반복
x cursor 문자 삭제 (count 지원)
D 줄 끝까지 삭제
dd 줄 전체(단일 라인 버퍼) 삭제
dw · db · de 단어 단위 삭제 (count 지원)
r + 문자 cursor 문자 치환 (normal 유지)
s cursor 문자 삭제 후 insert 진입
~ cursor 문자 대소문자 토글
yy 줄 전체 yank
p · P cursor 뒤 / 앞에 paste (count 지원)
u undo (insert 세션은 1 단위, 최대 100단계)
. 마지막 변경 명령 dot-repeat (count 포함)
! 버퍼가 비어 있을 때만 — shell 명령 입력 시작 (아래 !shell)

숫자 prefix 1-9999가 count가 된다 (2dw, 5l …). normal 모드에서 위 표에 없는 키는 무시된다(입력되지 않음).

입력줄에 !로 시작하는 줄을 Enter하면 shell 명령으로 실행된다 (모델 실행 중이 아닐 때만 — 실행 중이면 “wait for the run to finish” 알림).

  • 결과(출력 + [exit code N])는 user message로 트랜스크립트에 주입된다 — 다음 모델 run에서 모델이 그 결과를 직접 본다 (Qwen fenced 형식).
  • permission 게이트를 거치지 않으며, 실행 결과가 자동으로 모델 run을 트리거하지는 않는다.
  • 빈 !은 사용법 알림만 표시한다.
  • !shell과 모델의 Bash 도구는 명령을 터미널과 떼어 실행한다. 비밀번호를 터미널에서 묻는 명령(sudo, 저장된 비밀번호가 없는 git·ssh)은 기다리지 않고 바로 실패한다. 시간 초과(기본 2분)나 esc 때는 명령이 띄운 프로세스까지 함께 끝난다. 뒤로 띄운(&) 프로세스는 기다리지 않는다. 명령이 끝나고 1초 뒤에 결과가 오며, 그 프로세스는 계속 실행된다.
  • 모델의 Bash 도구는 앞에서 도는 10초 이상의 sleep(sleep 240, sleep 1m, 반복 안의 sleep 60 등)을 실행하지 않고 바로 돌려보낸다. 정해진 시간을 다 기다리는 대신 until(정규식)과 interval_s(기본 10초, 1~300)를 쓰게 한다. until을 주면 명령을 그 간격으로 다시 돌리다가 출력이 맞는 순간 끝나고, timeout_ms(최대 10분) 안에 맞지 않으면 마지막 출력과 (until: no match …)를 돌려준다. 그사이 모델은 부르지 않는다. 따옴표 안 글자, 10초 미만, 뒤로 띄운(&) sleep, 사용자가 직접 친 !명령은 막지 않는다.

답변이 성공적으로 끝나면 evo가 같은 모델로 짧은 호출을 한 번 더 해 사용자가 다음에 입력할 만한 문장을 만들고, 입력창이 비어 있으면 그 자리에 흐리게 보여 준다(… (Tab)).

  • Tab 또는 →: 제안을 입력창에 채운다. 제출하지 않으므로 고친 뒤 Enter로 보낸다.
  • 글자를 입력하면 제안이 가려지고, 입력을 모두 지우면 같은 제안이 다시 보인다. 여러 줄 입력의 빈 줄에서는 보이지 않는다. 입력하는 동안 도착한 제안은 표시하지 않는다.
  • 빈 입력창에서 Esc를 누르거나 새 실행이 시작되면 제안이 사라진다. 아직 오지 않은 제안 호출은 취소된다.
  • 오류·인터럽트로 끝난 실행, 대기열에 다음 메시지가 있을 때, goal 진행 중에는 제안하지 않는다.
  • 제안 호출은 도구 없이 최근 사용자 메시지 몇 개와 마지막 답변만 보낸다. 한 줄이 아니거나 너무 길거나 서식이 섞인 답은 표시하지 않는다.
  • 제안 호출의 사용량(토큰·추정 비용)은 답변마다 한 번씩 세션 합계(상태줄·/cost)에 더해진다. 표시하지 않고 버린 제안도 호출은 일어났으므로 합산된다.
  • 기본은 켜짐이다. ui.promptSuggestion: false(또는 /config의 “다음 질문 제안”)로 끈다.

agent가 호출하는 Bash tool에 pty: true를 넘기면 실 PTY(24×80)에서 실행된다 — REPL·isatty 감지 등 인터랙티브 프로그램의 stdin/stdout 왕복이 동작한다 (unix. Windows는 ConPTY 할당은 되지만 v1은 stdin 왕복 미지원). 출력은 ANSI 이스케이프 제거 후 pipe 경로와 동일한 형식으로 반환된다. PTY 할당이 실패하면 pipe 경로로 fallback하고 결과에 (PTY unavailable, fell back to pipe execution)을 붙인다. 기본(생략)은 기존 pipe 경로다 — pty: true가 없는 호출의 동작은 변경되지 않는다.

evo -p "prompt" [--output-format text|json|stream-json]
[--max-turns N] [--allowed-tools …] [--permission-mode …] [--model …]
[--resume [id] | --continue | --fork id|latest] [--no-wait]
[--dispatch-name 이름]
플래그 의미
-p, --print headless 실행. 프롬프트 필수 (빈 값은 사용법 오류)
--model 모델 테이블의 모델 id (기본: 설정의 "model")
--resume / --continue 세션 복기 / 최근 세션 계속 (호환 불가). id 없는 --resume(맨 끝이거나 다음이 다른 플래그)은 TUI를 열고 바로 /resume 세션 목록을 띄운다. Enter는 고른 세션으로 이어 가고 Esc는 새 세션으로 남는다. -p와 함께 쓰면 id가 필요하다는 사용법 오류(exit 2)다. 다른 evo가 쓰고 있는 세션이면 시작하지 않고 evo --fork <id> 안내와 함께 exit 1로 끝난다
--fork <id|latest> 세션을 새 세션으로 복사해 그쪽에서 이어 간다(원래 세션은 그대로). latest는 이 폴더의 가장 최근 세션이다. --resume·--continue와 함께 쓰면 사용법 오류(exit 2). TUI에서도 쓸 수 있다
--no-wait headless 전용. 실행이 끝나도 백그라운드 서브에이전트 작업을 기다리지 않고 바로 끝내며, 아직 돌던 작업 id를 stderr에 evo: exiting with N background task(s) still running (ids: …)로 알린다. 남은 작업은 프로세스와 함께 끝난다. 기본은 모두 끝날 때까지 기다린 뒤 결과를 알린다
--dispatch-name <이름> 이 실행의 evo-dispatch 이름(작업 담당)을 정한다. TUI와 -p 모두 쓴다. 설정의 dispatch_agent보다 우선하고 설정 파일은 바꾸지 않는다. 자세한 것은 evo-dispatch 연동
--debug TUI 전용, 그 실행만(저장하지 않음). 화면이 멈추는 곳을 <config-dir>/debug/stall-<시각>-<pid>.log(0600)에 남긴다: 0.1초 넘게 걸린 화면 처리의 종류(Go 타입 이름)와 시간, 0.5초 넘게 멈추면 그 순간 모든 goroutine이 기다리는 위치를 한 번, 첫 화면 처리까지 걸린 시간. 메시지 내용·입력한 글·명령줄·환경 변수는 남기지 않는다. 시작할 때 알림줄에, 끝날 때 stderr에 경로를 알린다. 켰을 때 화면 처리 한 번에 약 0.14µs가 더해진다
--permission-mode default | acceptEdits | plan | auto | bypassPermissions. 없으면 설정을 따르고, 설정도 없으면 TUI는 auto, -p는 default. 대소문자와 -·_·공백은 무시하고 화면 이름도 받는다(edits mode = acceptEdits)
-y, --yolo --permission-mode bypassPermissions의 별칭(Qwen Code·Gemini CLI와 같은 -y). 확인 없이 모든 tool을 실행하고 OS 샌드박스를 끈다. TUI는 시작할 때 확인 대화상자를 먼저 띄운다: 위험을 설명하고 1 아니오, 취소(기본) / 2 예, 위험을 이해했습니다 / 3 예, 이해했고 다음부터 묻지 않기를 줄마다 보여 주며, 열린 직후 0.5초 동안은 예 키를 무시하고 Enter는 기본 행(취소)에서 수락하지 않는다. 취소하면 evo가 종료된다. 3번은 ~/.evo/settings.json의 bypassPermissionsAccepted: true로 저장되며 사용자 설정 파일에서만 읽는다(프로젝트 설정의 값은 무효이고 공유 .evo/config.json에 두면 신뢰 게이트가 거부한다). 비대화형(-p)은 대화상자 없이 stderr에 경고를 낸다. root(UID 0)로 실행하면 사용법 오류(exit 2)로 거부한다. 다른 --permission-mode와 함께 쓰면 사용법 오류. 조직이 관리형 설정 permissions.disableBypassPermissionsMode("disable" 또는 true)로 금지하면 --yolo, --permission-mode bypassPermissions, TUI의 /permissions bypassPermissions가 모두 거부된다
--max-turns agent 턴 상한 — 모델 호출 횟수를 센다. 0(기본)이면 30. 초과 시 실행은 중단되고 exit code 3을 반환한다 (성공이 아님). 오류 메시지에는 실제 적용한 상한이 나온다(max turns exceeded (30)). CI에서는 토큰/예산 제한의 1차 방어선으로 사용한다. 비대화형(-p) 전용이다. 대화형 화면은 기본으로 턴 상한이 없고(사용자가 esc로 멈춘다, Claude Code·Qwen Code와 같다), 설정 "maxTurns": 50(양의 정수)을 두면 질문당 그 횟수에서 멈춘다. 서브에이전트는 어느 쪽이든 30이다
--output-format text (기본) | json | stream-json
--allowed-tools 프롬프트 없이 자동 허용할 tool 규칙. permission 규칙 문법(Bash(npm run test:*), Edit(src/**), Read …)이며 괄호 바깥의 쉼표/공백으로 분리된다. deny/ask 규칙은 여전히 우선한다
code 의미
0 성공
1 실행 오류 (provider 실패, run 실패, 설정 해석 불가 등)
2 사용법 오류 (잘못된 플래그, 플래그 값, 플래그 조합 — 예: 빈 -p, 미지의 --output-format, --resume+--continue 동시 지정)
3 예산/턴 초과 (--max-turns 초과)
  • text (기본): 최종 답변을 stdout, run 요약(세션 id·턴·토큰·비용)을 stderr.
  • json: 실행 종료 시 stdout에 결과 객체 1개. type: "result", subtype, is_error, num_turns, duration_ms, terminal_reason, session_id, model, result (최종 답변), usage, cost_usd.
  • stream-json: wire 이벤트 1개당 stdout 1줄 (NDJSON). 각 라인은 이벤트 프로토콜 v1 스키마(Final Report §6.1) — system/init → user → assistant → user(tool_result) → … → result. 라인의 uuid/timestamp은 세션 JSONL 트랜스크립트에 기록된 값과 동일하다.

json/stream-json 모드에서는 stdout에 기계가 읽는 출력만 작성된다(요약·오류 메시지는 stderr).

evo chat [--model id] [prompt...]
  • 모델 테이블의 모델 하나와 도구 없이 대화한다. 파일 읽기·셸 실행은 하지 않고, 세션 기록도 남기지 않는다.
  • 인자로 프롬프트를 주면 한 번 답하고 끝난다. 인자가 없으면 한 줄에 프롬프트 하나씩 받는 대화를 이어 가며, Ctrl-D로 끝낸다.
  • --model이 없으면 설정의 "model"을 쓴다. 둘 다 없으면 /provider로 모델을 정하라는 오류로 끝난다.

하나의 빌드로 일반 사용자와 조직 사용자를 함께 지원한다. 둘을 가르는 것은 조직 키 파일이다.

상태 동작
키 파일 없음 일반 모드 — 지금처럼 첫 실행 설정·/provider로 프로바이더를 직접 고른다
키 파일 있음 조직 모드 — 키 파일의 인증 서버로 인증하고, 인증 결과의 조직 프로필로 모델을 자동 구성한다
  • 위치: ~/.evo/org.json. 환경변수 EVO_ORG_FILE로 경로를 바꿀 수 있다.
  • 형식(비밀 값을 두지 않는다, 서명본 안의 내용):
    { "org": "acme", "name": "ACME", "auth": { "url": "https://auth.acme.example/realms/acme", "client_id": "evo-cli" }, "api": { "url": "https://evo-api.acme.example" }, "expires": "2027-12-31" }
    org·auth.url(OIDC 발급자 주소)·auth.client_id(공개 클라이언트 ID, 비밀 아님)·api.url(evo-api 주소, 아래 “evo-api 세션”)은 필수, name(화면 표시)·auth.scopes(요청 scope 목록, 기본 openid profile email, 서버가 지원하면 offline_access 추가)·expires(YYYY-MM-DD, 그날까지 유효)·update.url(evo update가 쓸 Nexus 서버 주소, http(s)와 URL에 안전한 글자만, )·dispatch.url(세션 간 dispatch 서버 주소, https://와 URL에 안전한 글자만, /mcp로 끝남)은 선택이다. update.url이 있으면 evo update는 EVO_NEXUS_URL이 없을 때 빌드 기본값 대신 그 주소에서 받는다. 키 파일이 열리지 않으면(서명 오류·만료 등) 빌드 기본값을 쓴다. dispatch.url이 있으면 조직 모드에서 그 주소를 dispatch 서버로 기본 연결한다(아래 evo-dispatch 연동 “조직용 기본 연결”). update.url·dispatch.url의 형식이 틀리면 다른 형식 오류와 같이 시작 오류다. evo 코드에는 기본 모델 게이트웨이가 없으므로 조직 모델은 항상 evo-api 세션에서 온다. 키 파일은 서명해서 배포하며, 서명이 없거나 맞지 않으면 시작 오류다(, org-keyfile.md). 위 형식은 서명본의 payload에 들어가는 내용이다. 형식 오류·필수 항목 누락·만료는 시작 오류이며 일반 모드로 넘어가지 않는다.
  • 조직 프로필은 모델 서버(엔드포인트) 목록이다. 엔드포인트마다 프로바이더 종류(litellm·openai(vLLM 등 OpenAI 호환)·anthropic·ollama)와 주소를 담고, 허용 모델을 적지 않으면 그 서버의 모델 목록(/v1/models, ollama는 /api/tags)을 조회해 채운다. 조회한 목록에 모델 크기(max_model_len, context_length, context_window, max_context_length, LiteLLM의 max_input_tokens 순)가 있으면 그 값을 모델의 context window로 쓴다. 크기가 없는 모델은 0(알 수 없음)으로 남으므로 서버 쪽에 크기를 설정해 둔다. anthropic 엔드포인트는 모델 목록을 프로필에 적어야 한다.
  • 조직 모드에서는 사용자 설정의 models를 쓰지 않는다. 설정의 model이 허용 목록 안이면 기본 모델로, 아니면 조직 프로필의 기본 모델(evo-api 세션의 default_model), 그것도 없으면 첫 허용 모델로 시작한다. 첫 실행 설정과 /provider는 열리지 않고 안내만 나오며, /model은 허용 모델 선택 창(↑/↓, Enter)을 열고, /model <id>로도 바꾼다. 서버에서 다시 조회하지 않는다. 고른 모델은 ID만 사용자 설정 model에 저장해 다음 시작의 기본이 된다. 모델 행·키·주소는 쓰지 않는다. -p의 --model도 허용 목록 안에서만 받는다.
  • 시작 화면과 /status에 조직 이름이 나온다.
  • Langfuse 수집은 항상 켜지고 사용자 설정으로 끌 수 없다. 질문(명령) 단위 trace 아래 모델 호출(generation)과 도구 실행(span)까지, 프롬프트와 도구 출력을 포함해 기록한다(max_output_chars로 자름). evo는 Langfuse에 직접 보내지 않고 키 파일 api.url의 evo-api POST {api.url}/api/cli/otel/v1/traces로 OTLP/HTTP JSON span을 로그인 토큰(Authorization: Bearer)과 함께 보낸다(Langfuse v4는 OTLP만 받는다). evo-api가 사용자 값을 토큰의 사용자로 덮어쓰고 자기 Langfuse 키로 전달한다. Langfuse 키는 사용자 PC에 두지 않는다(프로젝트 키는 프로젝트 전체를 읽을 수 있다). 로그인 전 기록은 버리고, 전송 실패는 한 번 재시도 후 버린다(모델 사용에는 영향 없음). evo 키에는 LiteLLM의 Langfuse 연동을 켜지 않는다(모델 호출이 두 번 기록된다).

인증은 표준 OIDC다(OIDC Discovery, 디바이스 코드 로그인 RFC 8628, refresh token, userinfo). 특정 인증 서버 전용 기능은 쓰지 않으므로 Keycloak·authentik 등 표준을 지키는 서버면 된다.

  • evo login 또는 TUI의 /login: 디바이스 코드 로그인을 한다. 확인 주소와 코드를 보여 주면 아무 브라우저에서나 그 주소를 열어 로그인하고 코드를 승인한다. 원격 터미널(code-server 등)에서도 된다. 로그인한 상태에서 TUI /login을 부르면 바로 로그인하지 않고 already signed in 선택 창을 띄운다. sign in again을 고르면 다시 로그인해 다른 계정으로 바꾸고, Esc(또는 keep)는 지금 로그인을 그대로 둔다. evo login은 바로 다시 로그인한다. 로그인 성공 안내(signed in to …, … 에 로그인했습니다)에는 계정(이메일 등)을 보여 주지 않는다. 로그인 상태와 계정은 /status의 login: 줄에서 본다: signed in to <조직 또는 evo> as <이름> (<계정>)(이름은 evo-api가 준 표시 이름, 계정은 이메일·없으면 사용자 이름. 이름이 없으면 as <계정>만,. 이름은 로그인·세션 갱신 때 저장되므로 그 전 세션은 계정만 보인다), 로그인하지 않았으면 not signed in to … — run /login. 저장된 세션 파일만 읽어 네트워크 요청이 없고, 토큰·키·서버 주소는 보이지 않는다. evo 로그인을 쓰지 않는 실행에는 이 줄이 없다. 코드가 들어간 확인 주소를 브라우저로 자동으로 연다. 환경변수 BROWSER가 있으면 그 명령으로 연다. code-server·VS Code 원격 터미널은 이 변수로 사용자 쪽 브라우저를 열며, 열기 전에 확인 팝업이 뜬다. BROWSER가 없으면 로컬 데스크톱(macOS·Windows, 화면이 있는 Linux)에서 SSH 접속이 아닐 때만 기본 브라우저로 열고, SSH 접속이나 화면 없는 Linux에서는 주소만 보여 준다. 열지 못해도 로그인 대기는 계속된다.
  • evo logout 또는 TUI의 /logout: 저장된 로그인과 evo-api 세션, 저장된 모델 목록을 지운다.
  • 대화형 실행은 유효한 로그인이 없어도 화면을 연다. 모델 없이 “/login으로 로그인하라”는 안내가 나오고, 입력을 보내도 실행하지 않고 같은 안내를 다시 보여 준다. /login은 화면 안에 주소와 코드를 보여 주고 승인을 기다리며(Esc로 취소), 끝나면 조직 모델로 바로 쓸 수 있다. 로그인이나 evo-api 세션 발급이 실패해도(예: evo-api가 503으로 답함) 화면은 연다. 이때 안내에 실패 원인을 함께 보여 주고, 서버가 복구되면 /login으로 다시 시도한다. evo-api가 오류 코드를 주면 오류 메시지에 HTTP 503 (key_service_unavailable)처럼 함께 나온다. 화면 없는 실행(-p)은 이 오류로 끝난다. 로그인 전에 연 화면의 기록은 로그인 뒤부터 전달되고, Langfuse 사용자 값은 evo-api가 정한다. 일반 모드(키 파일 없음)의 /login·/logout은 아래 “일반 사용자 evo 로그인”을 따른다.
  • 화면 없는 실행(-p)은 기다리지 않고 evo login을 안내하는 오류로 끝난다.
  • 토큰은 ~/.evo/org-token.json(0600)에 저장하고, 만료되면 refresh token으로 갱신한다. 갱신이 거절되면 다시 로그인해야 한다. 키 파일의 발급자·클라이언트가 바뀌면 저장된 토큰은 쓰지 않는다. 토큰 값은 화면·로그·오류에 나오지 않는다.
  • 로그인한 사용자(email, 없으면 preferred_username·sub)가 Langfuse 사용자 식별값이 된다.

인증 서버 관리자 설정

  • 공개 클라이언트(클라이언트 인증 끔, secret 없음)를 만들고 키 파일의 auth.client_id와 같은 ID를 준다.
  • 그 클라이언트에 OAuth 2.0 Device Authorization Grant를 켠다. 비밀번호 직접 입력(Direct access grants)은 켜지 않는다.
  • 로그인 유지를 위해 offline_access scope를 허용한다.

세션 엔드포인트의 api_key는 모델 목록 조회와 호출 모두에 그 키만 쓰고 환경변수 키는 보지 않는다(개인 모드에서 쓰던 OPENAI_API_KEY·LITELLM_API_KEY 등이 조직 서버로 가지 않게). 사용자 정보(userinfo)의 claim으로 프로필을 만들거나 기본 프로필로 대신하는 경로는 없다. 키 파일 서명은 org-keyfile.md를 따른다.

로그인 토큰으로 키 파일 api.url의 evo-api POST {api.url}/api/cli/session을 호출해(본문 client·version·device=호스트 이름) 그 응답을 조직 프로필로 쓴다.

{ "user": { "username": "hong", "display_name": "홍길동", "email": "hong@acme.example" },
"endpoints": [ { "provider": "litellm", "base_url": "https://litellm.acme.example/v1", "api_key": "<사람별 LiteLLM 키>" } ],
"default_model": "…", "api_key_expires_at": "2026-10-16T09:00:00Z" }

evo-api는 로그인을 확인하고 사람별 LiteLLM 키(허용 모델·예산·만료)를 설정해 돌려준다. evo는 그 키로 LiteLLM을 직접 호출하고, 모델 목록은 그 키로 LiteLLM /v1/models를 조회해 얻는다(키에 허용된 모델만 나온다). 응답에 models가 있으면 조회 대신 그 목록을 쓴다.

  • endpoints(엔드포인트마다 provider·base_url·api_key 필수)와 api_key_expires_at(RFC 3339)은 필수다. 사용자 식별값은 email, 없으면 username·display_name이다. 받은 키만 쓰고 환경변수 키는 보지 않는다.
  • 세션은 ~/.evo/org-session.json(0600)에 저장하고, 같은 api.url·발급자·클라이언트이며 만료까지 여유(1시간, 세션 수명이 4시간보다 짧으면 수명의 1/4)가 있으면 다음 시작에서도 다시 받지 않는다. evo-api는 같은 device가 세션을 다시 받으면 이전 키를 폐기하므로, 여러 evo 창이 이 파일 하나를 함께 쓴다.
  • 조회한 모델 목록은 ~/.evo/org-models.json(0600)에 저장한다. 다음 시작은 이 목록으로 바로 열고, 서버 조회는 백그라운드로 한다. 목록이 바뀌었으면 저장하고, TUI는 그 자리에서 새 목록으로 바꾼다. 쓰던 모델이 목록에서 빠졌으면 새 기본 모델로 바꾸고 안내한다. 조회가 실패하면 저장된 목록을 그대로 쓰고 아무것도 보이지 않는다. -p·evo chat은 그 실행의 목록을 바꾸지 않고 다음 시작을 위해 저장만 한다. 저장된 목록이 없거나 다른 계정·엔드포인트의 것이면 지금처럼 조회한 뒤 시작한다. 엔드포인트가 여럿이면 동시에 조회한다.
  • 이 파일에는 키·토큰을 넣지 않는다. 계정과 엔드포인트 주소는 SHA-256 값으로만 두고, 모델 이름·context window·이미지 지원 여부만 적는다.
  • 키는 쓸 때마다(실행마다) 만료를 확인해, 여유가 없으면 먼저 파일을 다시 읽고(다른 창이 갱신했을 수 있다) 그래도 오래됐으면 저장된 로그인(필요하면 refresh)으로 새 세션을 받는다. 갱신 중에는 디바이스 로그인을 하지 않으며, 로그인이 없으면 /login·evo login을 안내한다.
  • 만료 전에 키가 폐기돼(evo-api가 폐기, 다른 창이 새 세션을 받음 등) LiteLLM이 401을 돌려주면 그 자리에서 키를 바꾼다. 먼저 세션 파일을 다시 읽어 다른 창이 받은 새 키가 있으면 쓰고, 없으면 저장된 로그인(로그인 토큰이 만료됐으면 refresh)으로 새 세션을 받는다. 그다음 같은 요청을 한 번만 다시 보낸다. 새 키도 401이면 더 시도하지 않고 오류를 보여 준다. 로그인이 없거나 만료돼 갱신이 거절되거나 evo-api가 새 키를 거부하면, 디바이스 로그인 없이 거부 이유와 /login 안내를 보여 준다. OpenAI·Ollama 방식 행에 적용되며, Anthropic 방식 행은 응답 도중에 401이 오므로 만료 기준 갱신만 한다.
  • 오류: 401은 다시 로그인 안내, 403 cli_disabled는 CLI 사용이 꺼졌다는 안내, 그 밖의 403은 계정 권한 없음 안내, 429는 잠시 뒤 재시도 안내, 그 밖은 HTTP 상태를 보여 준다. 토큰·키 값은 오류에 나오지 않는다.

조직 키 파일이 없는 일반 사용자도 evo에 로그인하면 일반용 evo-api가 내주는 evo-lite 모델을 바로 쓸 수 있다.

  • 로그인해야 evo를 쓴다: 로그인은 evo 프로바이더가 아니라 Evo CLI를 쓰기 위한 것이다. 개인·조직 사용자 모두 evo는 실행되지만, 로그인하지 않으면 모델이 없다. 설정 파일에 넣어 둔 모델도 로그인 뒤에만 쓴다(행은 지우지 않는다). 로그인하면 evo 프로바이더(evo 로그인 모델)도 쓸 수 있다.

    • TUI: 첫 실행 메뉴와 /provider 메뉴에 evo — sign in만 보이고, 채팅은 /login 안내로 막힌다. /provider add·use·key는 “/login 먼저” 안내로 거부된다.
    • evo -p·evo chat: 요청을 보내지 않고 not signed in — run `evo login` to use evo 오류로 끝난다.
    • 저장된 로그인이 있어도 인증·세션이 실패하면 로그인 안 된 것으로 본다. 로그인은 됐는데 evo가 내준 모델이 없으면 사용자 모델만 쓴다.
    • 로그인하면 사용자 모델과 evo 모델을 함께 쓰고 anthropic·openai·openai-compatible·ollama·litellm을 추가·변경할 수 있다. /logout하면 다시 모델이 없어진다.
    • 개인 로그인이 꺼진 빌드(아래 -X main.personalAuthURL= 비움)는 로그인할 방법이 없으므로 설정 파일의 모델을 그대로 쓴다.
  • 로그인: TUI의 /login, 첫 실행 메뉴와 /provider 추가 메뉴 맨 위의 evo — sign in (no key needed), 또는 evo login. 조직 로그인과 같은 디바이스 코드 로그인이고 키를 입력하지 않는다. 로그인한 뒤에는 그 메뉴 항목이 evo — signed in (Enter picks a model)로 바뀌고, Enter를 누르면 evo 모델 선택 창이 열린다.

  • 로그인하면 evo 모델이 사용자 모델 표 뒤에 더해지고 evo 기본 모델로 바뀐다. 다음 시작부터는 사용자 설정의 model이 기본이고, 설정한 모델이 지금 모델 표에 없으면(서버 목록에서 빠짐 등) 시작 오류 없이 evo 기본 모델로 시작한다. evo 모델을 받지 못했으면 사용자 모델 표의 첫 모델로 시작한다.

  • 같은 이름의 사용자 모델: litellm 등으로 evo와 같은 모델을 직접 넣어 둔 경우.

    • 이름과 주소(끝의 /·대소문자 무시)가 같으면 같은 모델로 본다. 로그인한 동안 한 줄로 합쳐 evo 로그인 키로 부르고, 사용자 행에만 있는 context_window·auto_compact_tokens·vision·가격은 이어받는다. 설정 파일의 사용자 행은 그대로 둔다. 로그인 안내 줄 끝에 합쳐진 모델 이름이 나온다.
    • 이름은 같고 주소가 다르면 다른 모델로 본다. evo 모델은 evo/<이름>으로 따로 들어가고, 서버에는 원래 이름으로 요청한다.
    • 그래서 로그인한 상태에서 /provider나 첫 실행 메뉴의 evo를 고르면 로그인을 다시 하지 않고 evo 모델 선택 창이 열린다.
  • /logout(또는 evo logout)은 로그인을 지우고 모델이 없는 상태로 돌아간다.

  • 저장된 로그인이 없으면 시작할 때 인증 서버에 접속하지 않는다. 저장된 로그인이 있는데 인증이 실패해도 TUI 시작은 막지 않고 로그인 안 된 상태로 열린다.

  • evo 세션 키는 설정 파일에 저장되지 않는다. evo 모델을 쓰는 중에 /model은 evo 모델 선택 창을 열고(서버 조회 없음), 고른 모델의 ID만 사용자 설정 model에 저장한다(다음 시작의 기본). 저장에 실패하면 이번 세션에만 바뀌고 알림에 붙는다. /provider·/model 선택기는 evo 주소의 키를 물려받지 않는다. /provider 목록에서 키 출처는 signed in (no key needed)로 보인다.

  • 로그인 주소(일반용 Keycloak 발급자, 클라이언트 ID, 일반용 evo-api 주소)는 빌드 기본값이다. 기관용 빌드는 -X main.personalAuthURL=… -X main.personalClientID=… -X main.personalAPIURL=…로 바꾸고, 셋 중 하나라도 비우면 일반 사용자 로그인이 꺼져 /login·evo login은 조직 사용자 전용 안내만 한다.

  • 로그인·세션 파일은 조직 모드와 같은 ~/.evo/org-token.json·~/.evo/org-session.json(0600)을 쓰며, 발급자·주소가 다르면 서로 섞이지 않는다.

일반 모드에서 쓸 모델은 설정(~/.evo/settings.json 등)의 models 배열에 한 줄씩 적는다. model은 기본 모델의 id다. 없으면 첫 줄을 쓴다. /provider로 추가해도 같은 형식으로 저장된다. 조직 모드에서는 사용자의 models를 쓰지 않는다(조직 모드).

{
"model": "gpt-5",
"models": [
{"id": "gpt-5", "provider": "openai", "context_window": 400000, "pricing": {"in": 1.25, "out": 10}},
{"id": "qwen3.8:27b", "provider": "ollama", "auto_compact_tokens": 60000}
]
}
필드 필수 뜻
id 예 서버에 보내는 모델 이름. /model·--model에서 이 값으로 고른다
provider 아니오 evomind(기본) · openai · anthropic · ollama · litellm. 그 밖의 값은 시작 오류
protocol 아니오 openai · anthropic · ollama. 없으면 provider로 정한다: anthropic → anthropic, ollama → ollama(주소가 /v1로 끝나면 openai), 나머지 → openai
base_url provider에 따라 서버 주소. 없으면 아래 기본 주소. evomind·litellm은 기본 주소가 없어 꼭 적는다(evo는 /login으로 쓴다)
env_key 아니오 API 키를 읽을 환경변수 이름. 없으면 아래 기본 환경변수
key 아니오 설정 파일에 저장한 API 키. 환경변수에 값이 있으면 환경변수가 이긴다. ~/.evo/settings.json이나 .evo/config.local.json에 둔다
context_window 아니오 문맥 크기(토큰). 없으면 Ollama는 시작할 때 서버에 묻고, /provider로 추가한 모델은 서버 모델 목록의 크기를 쓴다. 그 밖은 알 수 없음(0)
vision 아니오 true/false. 이미지를 받는 모델인지(이미지)
auto_compact_tokens 아니오 이 모델의 자동 compact 시작 토큰 수(자동 compact)
pricing 아니오 {"in": …, "out": …}, 100만 토큰당 USD. 비용 추정에 쓴다(비용 추적)

필드 값의 형식이 틀리면(예: context_window에 글자) models[번호]와 필드 이름을 알리고 시작하지 않는다. 공유하는 프로젝트 설정 .evo/config.json에는 base_url·env_key·key를 둘 수 없다(두면 시작을 거부한다, 작업 폴더 신뢰).

provider별 기본값:

provider 기본 주소 기본 키 환경변수
evomind 없음(base_url 필수) EVO_API_KEY
openai https://api.openai.com/v1 OPENAI_API_KEY
anthropic https://api.anthropic.com ANTHROPIC_API_KEY
ollama http://localhost:11434 (OpenAI 방식이면 /v1을 붙임) 없음(키 없이 씀)
litellm 없음(base_url 필수) LITELLM_API_KEY

키 우선순위: env_key(또는 기본) 환경변수 값 → 모델 줄의 key. 둘 다 없으면 그 모델을 고를 때 어느 환경변수나 key를 설정하라는 오류가 난다.

TUI와 headless 모두에 등록되는 내장 tool 두 개.

공개 http(s) URL을 가져와 텍스트로 반환한다. HTML은 markdown으로 변환(제목·링크·코드 블록·목록·인용·테이블), text/markdown·plain·JSON·XML은 통과, 바이너리(PDF·이미지 등)는 거부. 상한: 다운로드 1 MiB(초과 시 에러), 반환 50 000 자(초과 시 절단 + [truncated…] 공지). 리다이렉트는 최대 5회까지 따라간다.

WebFetch({"url": "https://docs.python.org/3/library/os.html"})

웹 검색 결과를 반환한다 (제목·URL·스니펫, 최대 10개). provider: Brave Search와 Tavily (개인 키, 연구 결과 반영), 그리고 LiteLLM 검색 중계 relay (검색 업체 키는 중계 서버에만 있다).

WebSearch({"query": "go http client", "num_results": 5})

provider가 미설정 상태에서도 tool은 등록되며, 최초 호출 시 설정 안내 에러를 반환한다 (설정 후 재시작 없이 바로 쓸 수 있는 환경변수 우선 규칙을 따른다).

세션 검색 예산: 세션당 기본 20회(web_search.max_per_session, env EVO_WEB_SEARCH_MAX_PER_SESSION > config). 소진 시 에러가 아니라 “이미 모은 정보로 계속하라” 공지를 반환한다 (Claude/Qwen 수렴 UX).

429/오류 처리: 429는 Retry-After/X-RateLimit-Reset 대기(최대 30초) 후 1회만 retry, 재실패 시 “지금은 재시도하지 마라” 공지. auth 에러(401/403/422)는 retry 없음 + key 확인 안내. 네트워크 에러는 1회 retry 후 “검색 없이 계속” 공지 — 3종류(key/쿼타/네트워크)를 메시지에서 구분한다.

요청 식별·응답 상한 : WebSearch는 User-Agent evo/<빌드 버전> (+websearch), WebFetch는 evo/<빌드 버전> (+webfetch)를 보낸다(버전 정보 없는 개발 빌드는 evo/dev). 중계 서버·프록시에서 User-Agent로 허용 규칙을 둔다면 evo/로 시작하는지로 본다. 검색 응답은 압축을 푼 뒤 4 MiB까지만 읽고, 넘으면 그 검색을 오류로 처리한다.

~/.evo/settings.json 또는 .evo/config.local.json:

{
"web_search": { "provider": "brave", "api_key": "BSA…", "max_per_session": 20 }
}
  • provider: brave (https://brave.com/search/api/, free $5 크레딧/월 — credit card 등록 필요) 또는 tavily (https://tavily.com/, free 1,000 credits/월 — 카드 불필요)
  • API key 우선순위: 환경변수 BRAVE_API_KEY / TAVILY_API_KEY (provider별 관용 명) > web_search.api_key
  • max_per_session: 세션당 검색 한도 (기본 20). env EVO_WEB_SEARCH_MAX_PER_SESSION이 우선
  • 공유 프로젝트 레이어(.evo/config.json)의 api_key는 trusted gate에서 거부된다 — 키는 user/local 레이어나 환경변수로만
  • key 없는 호출: websearch: brave provider requires an API key — set the BRAVE_API_KEY …

모델 provider 종류가 litellm(또는 조직 게이트웨이 evomind)이면, web_search.provider를 정하지 않아도 WebSearch가 그 모델의 주소·키로 게이트웨이 검색 중계를 쓴다. 판정은 응답으로 추측하지 않고 모델 표의 provider 종류로만 한다. 검색은 실행마다 그때의 모델 기준으로 정해지며(/model로 바꾸면 다음 실행부터 반영), 세션 검색 예산은 모델이 바뀌어도 이어진다.

POST {모델 base_url}/v1/search/{tool} # base_url이 /v1로 끝나면 /v1을 겹치지 않는다
Authorization: Bearer {모델 키}
{"query": "...", "max_results": 5}
→ {"object": "search", "results": [{"title", "url", "snippet"}]}
  • 게이트웨이에 LiteLLM 1.78.7 이상과 search_tools 설정이 필요하다. 검색 도구는 순서대로 시도한다. 기본 목록은 조직 게이트웨이에 등록된 ["tavily", "searxng", "duckduckgo"]이며 web_search.tool(이름 하나 또는 목록)로 바꾼다.
  • 목록의 한 도구가 401(키 오류) 외의 이유로 실패하면 — 도구 없음(404), 미허용(403), 한도·크레딧 소진(429·432 등), 서버 오류, 연결 실패, 응답 해석 실패 — 재시도 없이 다음 도구로 넘어간다. Tavily 크레딧을 다 쓰면 searxng가 받고, 다음 달 크레딧이 채워지면 다시 Tavily부터 쓴다. 결과 0건은 정상 답으로 본다. 401이면 바로 멈춘다(다른 도구도 같은 키라 실패한다).
  • 답한 도구는 결과 첫 줄에 (via searxng)처럼 표시된다. 한 번의 검색은 도구를 넘어가도 30초를 넘지 않고, 세션 검색 예산에서 1회로 센다. 모두 실패하면 no search tool answered (tavily: HTTP 432; searxng: HTTP 500; …)처럼 도구별 사유를 묶어 안내한다.
  • web_search.provider를 brave·tavily로 정해 두면 게이트웨이 모델에서도 그쪽을 쓴다.
  • 게이트웨이가 아닌 모델(Ollama·vLLM 직접 등)에서 중계를 쓰려면 "web_search": {"provider": "relay"}를 둔다. base_url·api_key를 생략하면 현재 모델의 주소·키를 쓰고, 다른 주소(web_search.base_url)의 중계에는 모델 키를 보내지 않으므로 web_search.api_key가 필요하다. web_search.base_url은 공유 프로젝트 레이어에서 거부된다(trusted gate) — user/local/managed 레이어에만 둔다.
  • wrapper 모델(claude-cli·copilot/<모델>·codex/<모델>)은 게이트웨이가 아니어서 중계를 저절로 쓰지 않는다. 그 행에는 모델 주소·키도 없다. 그래서 WebSearch를 쓰려면 web_search.provider를 brave·tavily로 두거나, relay면 web_search.base_url과 web_search.api_key를 모두 준다. 설정이 없으면 websearch: no provider configured 오류가 모델에 간다.
  • 도구를 하나만 둔 경우: 404는 게이트웨이에 그 검색 도구가 없음 → “검색 없이 계속” 안내, 403은 이 키에 검색이 허용되지 않음 → 관리자에게 요청, 재시도하지 않음. 429는 위 한도 처리와 같다.

모델 provider litellm은 /provider 메뉴(또는 첫 실행 설정)에서 고른다. OpenAI 프로토콜이며 주소를 직접 입력하고 키가 필요하다(기본 env LITELLM_API_KEY). 모델 표에 직접 쓸 때는 base_url이 필수다. 같은 endpoint에서 /model로 다른 모델을 골라도 provider 종류는 litellm으로 유지된다.

둘 다 read-only가 아니다 (외부 네트워크 읽기이므로) — 기본 모드에서 규칙이 없으면 ask(headless는 ask→deny):

규칙 의미
WebFetch / WebFetch(*) 전체 tool 허용
WebFetch(https://docs.python.org/*) URL glob (호스트·패턴별 허용/거부)
WebSearch / WebSearch(*) 전체 tool 허용 (v1에서 query는 permission 대상이 아니므로 다른 specifier는 매치하지 않음)
WebFetch (deny, 무범위) tool을 model에게 완전히 숨김

evo update와 evo update --check는 self-update이다. 설치 위치로 채널을 자동 감지한다:

채널 설치 형태 업데이트 동작
managed 한 줄 설치 레이아웃 — ~/.local/share/evo/versions/<ver> (POSIX) / %LOCALAPPDATA%\evo\versions\<ver>\evo.exe (Windows) 새 버전 다운로드 → sha256 검증 → --version smoke → pointer 스왑 (최근 3개 버전 유지 = 롤백 가능)
standalone PATH에 복사된 단일 바이너리 첫 업데이트 시 versioned layout으로 migration 후 pointer 스왑
npm npm install -g @evomind/evo npm install -g @evomind/evo@latest 위임
  • Binary 채널(managed/standalone): 최신 버전 조회는 Nexus의 evo-bin/evo-cli/release/latest/VERSION(CI가 릴리스마다 쓰는 태그 한 줄), 없거나 버전이 아니면 옛 evo-bin/evo-cli/release/latest, 그것도 없으면 예전 위치 evo-bin/install/latest 익명 읽기가 1차, 둘 다 없거나 버전이 아니면 Nexus asset search 익명 읽기. 받기는 evo-bin/evo-cli/release/<태그>/를 먼저, 없으면 루트 evo-bin/<태그>/, GitLab Release API(GET /releases?per_page=1)가 fallback이다 — GITLAB_TOKEN(또는 EVO_GITLAB_TOKEN)은 GitLab fallback에만 필요하고, 한 줄 설치(#34) 사용자는 token 없이도 evo update가 즉시 동작한다. 다운로도 Nexus raw(evo-bin) 익명 읽기. 최신 조회가 양쪽 모두 실패할 때만 에러(두 원인 포함)로 보고된다
  • base override: EVO_GITLAB_URL / EVO_GITLAB_PROJECT / EVO_NEXUS_URL
  • 개발판 : develop 머지마다 CI가 올리는 X.Y.Z-dev.N 빌드(install.sh --dev / install.ps1 -Dev로 설치). 개발판에서 evo update·--check·시작 알림은 같은 Nexus 서버의 저장소 evo-bin-dev에서 evo-cli/dev/latest/VERSION을, 없거나 개발판 버전이 아니면 옛 evo-cli/dev/latest를 읽고, 받기는 evo-bin-dev/evo-cli/dev/<버전>/에서 정식판과 같은 sha256 검증·--version smoke·pointer 스왑·GC로 한다(GitLab·asset search fallback 없음). --check 출력에 track: dev builds 줄이 붙는다. 비교는 X.Y.Z 다음 N 순이며, X.Y.Z-dev.N은 정식 X.Y.Z보다 앞(pre-release)이다
    • evo update --release: 정식 최신판으로 옮긴다(개발판 번호가 더 높아도 설치). evo update --dev: 정식판에서 최신 개발판으로 옮긴다. 둘은 함께 쓸 수 없고, --check와 함께 쓰면 그 줄의 최신판과 비교만 한다(옮길 판이 있으면 종료 코드 10). 옮긴 뒤에는 플래그 없이 evo update가 설치된 판의 줄을 따른다
    • npm 설치에는 개발판이 없다: --dev는 안내 오류(종료 코드 1). --release는 npm에서 평소와 같다
  • 기관용 빌드 : 폐쇄망 기관에 배포하는 바이너리는 같은 소스·같은 버전에서 빌드 매개변수(-X main.nexusURL=…, -X main.institution=<id>)만 다르다. 기관용 빌드는 evo --version, /version, /status에 institution <id>를 보여 주고(영문·숫자·.·_·-, 최대 32자, 그 밖의 값은 <invalid>), 일반 빌드의 출력은 달라지지 않는다. 버전 문자열은 X.Y.Z 그대로라 업데이트 판정에 영향이 없고, 식별자는 외부로 전송되지 않는다
  • 기관용 빌드 만들기 (릴리스 담당자용): 폐쇄망 기관 배포용 바이너리는 이미 만들어진 보호 태그의 파이프라인에서 수동 job release:institution으로 만든다. “Run manual job with variables”로 INSTITUTION_ID(영문·숫자·.·_·-, 최대 32자), INSTITUTION_NEXUS_URL(그 기관의 내부 Nexus 주소), TARGETS(선택, 기본 linux/amd64, 공백으로 구분)를 입력한다. 산출물은 dist/<기관>/evo_<태그>_<os>_<arch>와 dist/<기관>/sha256sums.txt이며(파일 이름은 일반 빌드와 같아야 evo update가 찾는다) 파이프라인 artifact에서 내려받아 폐쇄망으로 반입한다(sha256 확인). 기관 Nexus에는 evo-bin/<태그>/로 올린다. 회사 Nexus·GitLab Release·npm에는 올라가지 않고, 기관 목록·주소는 저장소에 남지 않는다. 같은 태그에 새 파이프라인을 만들지 말 것 — 일반 release job이 다시 게시된다
  • 폐쇄망: 외부 인터넷이 차단된 기관의 설치·업데이트(내부 Nexus 구성, 반입 설치, 건너뛴 업그레이드)는 폐쇄망 설치와 업데이트를 본다

의존성 확인: evo update(--check 제외)는 끝에 설치 저장소의 한 줄 설치기를 받아 --deps-only(Windows -DepsOnly)로 실행해 Node.js·glab·git을 처음 설치 때와 같은 규칙으로 맞춘다(README “함께 설치되는 도구”). 이미 최신이면 아무것도 하지 않고, 실패해도 업데이트 결과는 그대로이며 경고만 남긴다. --no-deps 또는 EVO_INSTALL_DEPS=none으로 끈다. TUI /update는 git을 빼고(sudo 비밀번호를 물을 수 없음) 출력은 대화 영역에 보인다.

TUI의 /update : TUI 안에서 /update는 evo update, /update check는 evo update --check와 같다(같은 채널 감지·설정). 출력은 대화 영역에 보인다. 실행 중에는 끝난 뒤에 할 수 있다.

  • 새 버전을 설치하면 evo가 새 버전으로 스스로 다시 시작한다. TUI를 정상 종료한 뒤 같은 플래그로 새 버전을 실행하고, 대화가 있던 세션은 --resume <세션 ID>로 이어 간다(원래의 --resume·--continue·--fork는 이 값으로 바뀐다). 세션 중 Shift+Tab·/permissions로 바꾼 권한 모드는 이어지지 않고 시작 모드로 돌아간다. 설치가 대화 실행 중에 끝나면 그 실행(과 대기열)이 끝난 뒤 다시 시작한다.
  • 다시 실행하는 파일은 Linux·macOS는 ~/.local/bin/evo(새 버전을 가리키는 링크), Windows는 포인터 파일이 가리키는 새 버전, npm 설치는 같은 경로다. 재시작에 실패하면 오류와 evo --resume <세션 ID> 안내를 출력하고 끝난다.
  • /update check, 이미 최신, 업데이트 실패는 다시 시작하지 않는다.

시작 시 업데이트 알림 : 대화형 TUI는 시작할 때 백그라운드에서 최신 버전을 확인하고, 새 버전이 있으면 대화 영역에 ⬆ evo <latest> is available (current <current>) — run evo update 줄을 보인다. 알림만 하고 설치는 하지 않는다(설치는 evo update).

  • 확인은 하루 한 번이다. 결과(실패 포함)를 <config dir>/update-check.json(개발판은 update-check-dev.json)에 24시간 캐시한다.
  • 개발판(X.Y.Z-dev.N)은 evo-bin-dev/evo-cli/dev/latest/VERSION(없으면 옛 dev/latest)과 비교해 더 새 개발판을 알린다. 다른 줄의 판(정식판↔개발판)은 알리지 않는다.
  • 바이너리 채널은 Nexus 익명 조회만 쓰고 GitLab 토큰은 쓰지 않는다. npm 채널은 registry를 조회한다.
  • 5초 안에 답이 없거나 실패하면 아무것도 표시하지 않는다(폐쇄망에서도 시작이 늦어지지 않음).
  • headless(-p)와 직접 빌드한 판(버전 dev, N 없는 X.Y.Z-dev)에서는 확인하지 않는다.
  • 끄기: settings의 "ui": {"updateNotice": false} 또는 환경 변수 EVO_NO_UPDATE_NOTIFIER=1. 현재 상태는 /config에 표시된다.
  • --check: 보고(stdout) 후 exit 0(최신) / 10(업데이트 가능) / 1(오류)
  • Windows: PATH의 evo.exe는 stable stub(설치 1회, 교체 없음) — pointer 파일(%LOCALAPPDATA%\evo\current)이 가리키는 payload를 re-exec한다. 실행 중.exe 교체·삭제는 구조적으로 없다(Windows file lock 회피)
evo uninstall # 지울 항목을 보여 주고 확인을 받는다
evo uninstall --yes # 묻지 않는다(~/.evo는 남김)
evo uninstall --yes --purge # ~/.evo까지 지운다
  • 설치 스크립트가 만든 것만 지운다: ~/.local/bin/evo 링크(설치된 버전 디렉터리를 가리킬 때만), ~/.local/share/evo(설치된 모든 버전), 셸 설정 파일(~/.bashrc·~/.zshrc·~/.profile)의 # >>> evo (one-line install) >>> 블록. 셸 설정 파일의 다른 줄과 권한은 그대로 둔다.
  • ~/.local/bin/evo가 다른 곳을 가리키는 링크나 일반 파일이면 지우지 않고 알린다.
  • ~/.evo(설정·로그인·세션 기록)는 따로 묻고 기본은 남긴다. --yes만 주면 남기고, --purge를 주면 지운다. 프로젝트 안의 .evo/는 건드리지 않는다.
  • 터미널이 아닌 곳에서 --yes 없이 실행하면 사용법 오류(exit 2)이며 아무것도 지우지 않는다.
  • npm으로 설치했으면 npm uninstall -g @evomind/evo 안내만 한다.
  • Windows는 실행 중인 evo.exe를 지울 수 없어서 지울 경로(%LOCALAPPDATA%\evo, %LOCALAPPDATA%\Programs\evo)와 사용자 PATH 항목만 안내한다.

EVO_CONFIG_DIR은 세션 기록 같은 실행 데이터의 위치만 바꾼다. 없으면 ~/.evo다. 설정·로그인 파일은 이 값과 상관없이 항상 ~/.evo에 있다.

<config-dir>(EVO_CONFIG_DIR, 없으면 ~/.evo) 아래에 두는 것:

  • 세션 트랜스크립트: projects/<path-slug>/<session-id>.jsonl (--resume·--continue·--fork가 여기서 찾는다)
  • subagent 기록: projects/<path-slug>/subagents/
  • auto memory: projects/<path-slug>/memory/ (아래 Auto memory 절)
  • TUI 입력 기록: history
  • 업데이트 알림 확인 캐시: update-check.json
  • /doctor의 쓰기 검사도 이 디렉터리에 한다. /status가 이 경로를 보여 준다.

항상 ~/.evo에 두는 것(EVO_CONFIG_DIR을 따르지 않음):

  • 사용자 설정 settings.json, 사용자 지침 EVO.md, 사용자 에이전트 정의 agents/
  • 조직 키 파일 org.json(경로는 EVO_ORG_FILE로 바꾼다), 로그인 토큰 org-token.json, 세션 정보 org-session.json, 저장된 모델 목록 org-models.json, 기본 dispatch 서버의 지난 도구 목록 dispatch-tools.json
  • 작업 폴더 신뢰 목록 trusted_folders.json
  • evo uninstall --purge가 지우는 것도 ~/.evo다. EVO_CONFIG_DIR을 다른 곳으로 두었다면 그 디렉터리는 따로 지운다.

설정 파일(settings.json, 권한 규칙을 저장하는 .evo/config.local.json)은 새 파일을 쓴 뒤 한 번에 바꾼다. 쓰는 도중 멈추거나 두 창이 같이 써도 이전 내용이나 새 내용 중 하나만 남는다. 파일 권한은 0600이다. 사용자 설정 파일이 심볼릭 링크면 링크 대상에 쓰고 링크는 그대로 둔다. 프로젝트 파일은 저장소가 정하므로, .evo 폴더나 .evo/config.local.json이 심볼릭 링크면 쓰지 않는다. 그때 “항상 허용(이 프로젝트)“은 이번 세션에만 허용되고 저장 실패가 함께 표시된다.

Claude Code·Gemini CLI·Codex처럼, 처음 여는 폴더에서는 그 폴더를 믿는지 묻는다. 내려받은 저장소의 .evo/ 설정(hooks, MCP 서버, LSP 명령, agents)이 사용자 권한으로 명령을 실행할 수 있기 때문이다.

  • TUI: 신뢰하지 않은 폴더(git 저장소 안이면 저장소 루트, 아니면 시작 폴더)에서 시작하면, 아무것도 읽거나 실행하기 전에 Accessing workspace: 확인 창을 띄운다. 폴더의 .evo/가 실행할 항목(hooks, MCP 서버 이름, LSP 명령, agents 수)도 함께 보여 준다.
    • 1. Yes, I trust this folder(기본, Enter): ~/.evo/trusted_folders.json(0600)에 저장하고 계속한다. 신뢰한 폴더의 하위 폴더는 다시 묻지 않는다.
    • 2. No, exit·Esc·Ctrl+C: 폴더의 설정을 하나도 실행하지 않고 끝난다.
    • 홈 디렉터리와 /는 저장하지 않아 열 때마다 묻는다.
  • 비대화형(-p): 묻지 않는다. 신뢰하지 않은 폴더이면 공유 프로젝트 설정 .evo/config.json의 hooks·mcp_servers·lsp와 .evo/agents/를 쓰지 않고 stderr에 한 줄로 알린다. 나머지(모델, 지시문 파일 등)는 그대로다. CI처럼 그 실행만 신뢰하려면 EVO_TRUST_WORKSPACE=1.
  • 개인 설정 .evo/config.local.json과 사용자 설정은 신뢰와 상관없이 쓴다. 공급자·권한 키를 공유 프로젝트 설정에서 거부하는 기존 규칙(trusted gate)도 그대로다.
  • 단, .evo/config.local.json이 git에 커밋된 파일이거나 폴더가 git 저장소가 아니면(zip·tarball로 받은 소스 등) 폴더와 함께 온 공유 설정으로 본다. 신뢰하지 않은 폴더에서는 config.json과 같이 hooks·mcp_servers·lsp를 쓰지 않고, 권한 설정 permissions·permissionMode·bypassPermissionsAccepted도 쓰지 않는다. git을 실행할 수 없는데 .git이 있으면 커밋 여부를 알 수 없으므로 커밋된 것으로 본다. git 저장소 안에서 커밋하지 않은 config.local.json만 본인 설정으로 보고 모두 쓴다.
  • 신뢰를 거두려면 ~/.evo/trusted_folders.json의 trusted 목록에서 그 경로를 지운다. 파일이 깨지면 아무 폴더도 신뢰하지 않은 것으로 보고 다시 묻는다.

훅은 이벤트마다 {"matcher", "command"} 객체의 목록이다(matcher는 도구 이벤트에서만 쓰는 정규식).

{ "hooks": { "PreToolUse": [{ "matcher": "Bash|Edit", "command": "~/hooks/check.sh" }] } }

Claude Code 형식({"matcher": "Bash", "hooks": [{"type": "command", "command": "..."}]})은 읽지 않는다. 그 형식이 있으면 시작할 때 has no command — the Claude Code form … is not read 오류가 난다. 각 훅을 위 형식으로 풀어 적는다. 여러 설정 층의 훅은 합쳐진다(조직 훅만 실행은 docs/managed-settings.md).

시작할 때 MCP 서버(mcp_servers)를 모두 동시에 연결한다. 그래서 기다리는 시간은 가장 느린 서버 하나만큼이다. 서버마다 연결 상한은 15초다(개인용·조직용 기본 dispatch는 3초). 상한에 걸리면 바로 실패로 돌아오고, 끊기 정리는 뒤에서 한다. 도구 순서와 이름이 겹친 도구 처리는 설정 순서를 따른다.

TUI는 MCP 서버를 기다리지 않고 첫 화면을 띄운 뒤 뒤에서 연결한다. 연결되기 전에는 /mcp에 connecting으로 보이고, /inbox는 연결 중이라고 알린다. 연결 전에 보낸 메시지에는 그 서버 도구가 없고, 연결된 뒤 다음 메시지부터 붙는다. 연결 중에 끝내면 연결을 바로 취소한다. -p는 사용자 설정의 서버를 기다린 뒤 시작한다(도구가 첫 요청부터 있어야 하므로). 개인용·조직용 기본 dispatch 서버는 지난 도구 목록이 저장돼 있으면 뒤에서 연결한다(아래 evo-dispatch 연동). 개인용 기본 버그 신고 공간(dispatch-bugs)은 언제나 뒤에서 연결한다.

연결할 때 보내는 클라이언트 정보(clientInfo)는 이름 evo와 빌드 버전이다. User-Agent와 같은 값이고, 버전 정보 없는 개발 빌드는 dev다.

MCP 서버(mcp_servers) 도구가 돌려준 텍스트는 50000자까지만 모델에 넘긴다. 넘으면 그 자리에서 자르고 [truncated: N more characters — the MCP tool result exceeded the 50000-character output cap]를 붙인다. 오류 결과(isError)의 문구도 같다. WebFetch 결과 상한과 같은 값이다. 결과가 잘리면 모델이 범위를 좁혀 다시 요청할 수 있다.

MCP 서버에 로그인 토큰 붙이기 (auth: "org")

섹션 제목: “MCP 서버에 로그인 토큰 붙이기 (auth: "org")”

사용자를 evo 로그인(Keycloak)으로 구분하는 사내 MCP 서버(예: 작업지시서 중계 서버 evo-dispatch)는 HTTP 서버 항목에 "auth": "org"를 둔다.

{"mcp_servers": [{"name": "dispatch", "url": "https://dispatch.example/mcp", "auth": "org"}]}
  • 그 서버로 가는 모든 요청(initialize·tools/list·tools/call 등)에 지금 로그인의 access token이 Authorization: Bearer …로 붙는다. 요청마다 토큰을 꺼내므로 만료됐으면 먼저 갱신하고, 갱신한 토큰은 ~/.evo/org-token.json에 저장한다. 새 디바이스 로그인은 시작하지 않는다.
  • 조직 모드는 조직 키 파일의 로그인을, 개인 모드는 evo 로그인을 쓴다.
  • 로그인이 없거나 갱신이 거부되면 그 서버만 연결되지 않고 /mcp에 not signed in — /login connects it으로 보인다. TUI에서 /login에 성공하면 그 서버에 다시 연결하고, 다음 실행부터 도구를 쓸 수 있다.
  • auth를 두지 않은 서버에는 토큰을 보내지 않는다.
  • 그 서버가 리다이렉트하면 같은 origin(스킴·호스트·포트)일 때만 따라간다. 다른 호스트나 https→http로 보내면 토큰을 싣지 않고 연결이 실패한다. 로그인 토큰 갱신과 dispatch 활성 신호 요청은 리다이렉트를 따라가지 않는다.
  • 그 밖에 키를 싣는 요청(모델 호출·모델 목록·웹 검색·텔레메트리·evo-api 세션·업데이트)은 리다이렉트를 따라가되, 처음 요청과 origin(스킴·호스트·포트)이 다르면 Authorization·x-api-key·X-Subscription-Token·PRIVATE-TOKEN을 빼고 보낸다. 같은 origin이면 그대로 싣는다.
  • 허용 위치: ~/.evo/settings.json과 managed 설정만. 프로젝트 설정(.evo/config.json, .evo/config.local.json)에 있으면 저장소가 토큰을 다른 주소로 보내게 할 수 있으므로 설정 오류로 시작하지 않는다.
  • 설정 오류로 거부하는 것: command(stdio) 서버의 auth, "org"가 아닌 값, headers의 Authorization과 함께 쓰기, https가 아닌 주소(http는 localhost·127.0.0.1·::1만).
  • 여러 evo 창이 같은 로그인을 쓰다 한 창이 refresh token을 먼저 바꿨으면, 다른 창은 갱신이 거부될 때 파일을 다시 읽어 새 토큰을 쓴다.
  • 한 evo 안에서는 토큰 갱신(telemetry, MCP·dispatch, 모델 키 세션)을 한 번에 하나만 한다. 기다린 쪽은 파일을 다시 읽어 이미 갱신된 토큰을 쓴다.
  • 토큰 값은 /mcp 상태, 오류 메시지, 세션 기록(JSONL·stream-json)에 나오지 않는다.

evo-dispatch는 내 evo 세션끼리 요청서를 주고받는 사내 MCP 서버다. auth: "org" 서버 항목에 "dispatch": true를 더하면 evo가 그 서버에 세션을 알린다. 규격: framework/evomind/web/workflow-project-evo-dispatch docs/protocol/client-protocol.md v1 r4.

{"mcp_servers": [{"name": "dispatch", "url": "https://dispatch.example/mcp", "auth": "org", "dispatch": true}],
"dispatch_agent": "CD01"}
  • 활성 신호: 세션이 열려 있는 동안 20초마다 같은 호스트의 /v1/presence(MCP url의 /mcp를 바꾼 주소)로 신호를 보낸다. TUI는 mode: interactive, -p는 headless다. -p는 첫 신호를 뒤에서 보내고, 첫 도구 호출 전에만 그것을 기다린다. 도구를 쓰지 않는 -p는 첫 신호를 기다리지 않는다. 서버는 마지막 신호 90초 뒤 세션을 비활성으로 본다.
  • 세션 교체: /clear·/compact는 rotate(이전 세션 ID와 이유)로 알려 안 읽은 요청을 새 세션으로 옮긴다. /resume은 이전 세션 end 뒤 새 세션 신호, --fork는 첫 신호에 원본 세션 ID(forked_from)를 넣는다. --resume·--continue는 같은 ID로 이어진다.
  • 종료: 끝낼 때 end를 보낸다. /update 뒤 새 evo가 같은 세션을 이어 받을 때는 보내지 않는다. end와 MCP 연결 닫기는 동시에 하고, 원격(HTTP) 서버는 최대 2초만 기다린다. 서버가 답하지 않아도 -p·TUI는 2초 안에 끝난다(전: -p 5초, TUI 9.5초). stdio MCP 서버는 프로세스가 끝날 때까지 기다린다. 닫을 때는 그 서버가 띄운 프로세스도 함께 끝낸다. npx·sh 같은 실행기 아래에서 도는 서버가 evo를 끈 뒤에 남지 않는다(Windows 제외).
  • 개인용 기본 연결: 개인용(조직 키 파일 없음) 빌드에 dispatch 주소가 들어 있으면, 설정 없이도 dispatch 서버(auth: "org", "dispatch": true)가 기본으로 붙는다. evo 로그인으로 서명하므로 로그인한 사용자만 쓴다. 사용자 설정에 "dispatch": true 서버가 있으면 그것을 쓰고 기본 항목은 넣지 않는다. 이 표시는 auth: "org"가 필요해서 사용자·관리 설정만 둘 수 있다. 이름만 dispatch인 다른 서버는 기본 항목이 그 이름을 대신 쓴다(저장소 설정이 dispatch 자리를 차지하지 못하게). 주소는 소스에 없고 빌드 때 넣는다(-X main.personalDispatchURL=…). 릴리스·개발판 CI는 보호·숨김 CI 변수 EVO_PERSONAL_DISPATCH_URL(evo-cli 프로젝트 변수)을 쓰고, 없으면 그룹 변수 EVO_DESKTOP_PERSONAL_DISPATCH_URL(데스크톱 앱과 같은 이름)을 쓴다(둘 다 없으면 넣지 않음, https://만), 기관용 빌드에는 넣지 않는다. 이 기본 서버의 연결 상한은 3초다. 같은 서버의 버그 신고 공간은 아래 절(dispatch-bugs)을 본다. 서버가 답하지 않으면 3초 뒤 그 서버는 dispatch 도구 없이 남고 /mcp에 오류로 보인다(TUI는 그동안 기다리지 않는다). 지난 도구 목록: 이 기본 서버에 연결될 때마다(TUI·-p) 그 도구 목록(이름·설명·입력 형식)을 ~/.evo/dispatch-tools.json(0600)에 저장한다. 서버 주소와 로그인한 사용자는 SHA-256 값으로만 넣고, 토큰은 넣지 않는다. 주소나 사용자가 다르면 쓰지 않는다. -p는 저장된 목록이 있으면 그 도구를 첫 모델 요청에 바로 넣고 연결은 뒤에서 한다. 모델이 도구를 부르면 그 전에 연결과 첫 신호를 기다린 뒤 실제 연결로 부른다. 도구를 쓰지 않는 -p는 dispatch를 전혀 기다리지 않는다. 저장된 목록이 없으면 연결을 기다린 뒤(최대 3초) 첫 요청을 보낸다. 연결해 보니 목록이 바뀌었으면 다음 요청부터 실제 목록을 쓰고 파일도 고친다. 없어진 도구를 부르면 오류 결과가 된다. 연결에 실패하면 저장된 도구를 불러도 “연결되지 않음” 오류가 되고 실행은 이어진다.
  • 조직용 기본 연결: 조직 모드(서명 검사를 통과한 조직 키 파일)에서는 빌드에 넣은 개인용 dispatch 주소를 쓰지 않는다. 대신 키 파일에 dispatch.url이 있으면 그 주소를 dispatch 서버(auth: "org", "dispatch": true, 연결 상한 3초)로 기본 연결한다. 조직 로그인으로 서명하므로 조직에 로그인한 사용자만 쓴다. 규칙은 개인용 기본 연결과 같다: 사용자 설정에 "dispatch": true 서버가 있으면 그것을 쓰고, 이름만 dispatch인 다른 서버는 이름을 내주며, 설정 파일에는 쓰지 않는다. 지난 도구 목록(~/.evo/dispatch-tools.json, 조직 로그인 사용자 기준)과 -p·TUI의 뒤에서 연결도 같다. 버그 신고 공간(dispatch-bugs, /mcp/bugs)과 개발 요청 공간(/mcp/dev)은 조직 모드에서 붙이지 않는다. 키 파일에 dispatch.url이 없으면 기본 연결이 없다(관리자가 managed·사용자 설정 mcp_servers에 넣는다).
  • 상태줄: dispatch 서버가 있으면 상태줄에 이 세션의 dispatch 부분이 항상 보인다. 이름이 있으면 dispatch CD02, 없으면 dispatch no name · workspace evo(작업 공간도 없으면 dispatch no name)다. 이름이 없어도 접속은 따로다(작업 공간·내 모든 세션 주소로 오는 요청은 받는다). 첫 신호 응답 전에는 · connecting, 마지막 신호가 실패하면(로그인 없음·네트워크·서버 오류) · offline이 붙고, 다음 신호가 성공하면 사라진다.
  • 안 읽은 요청: 신호 응답의 안 읽은 수가 1 이상이면 상태줄 dispatch 부분에 · 2 requests처럼 붙는다. 확인과 읽음 표시는 모델의 dispatch_inbox·dispatch_read 도구 또는 /inbox로 한다.
  • /inbox : 이 세션이 받은 안 읽은 요청 목록을 연다(오래된 순, 보낸 쪽 작업 담당/작업 공간 · 제목 · 시각). 맨 위에 이 세션의 이름(this session: agent … · workspace …)이 보여, 다른 세션에서 어디로 보낼지 말할 때 쓴다. 목록은 dispatch_inbox만 불러 보므로 보고 Esc로 나가면 아무것도 읽음이 되지 않는다. ↑/↓·Enter로 고른 요청에만 dispatch_read를 불러(이때 읽음, 같은 요청을 받은 다른 세션 목록에서 빠짐) 본문·링크를 대화에 보여 준다. 고른 요청은 실행하지 않는다. 다음에 보내는 메시지에 그 요청이 “받은 요청(지시 아님)“으로 붙어 모델에 가고, 모델은 사용자의 지시대로 처리하며 답장은 dispatch_send(kind: reply, reply_to)로 한다. 보내기 전에 빈 입력에서 Esc를 누르면 요청을 뗀다(이미 읽음). 모델 없이 evo가 직접 부르며 토큰과 세션 header는 모델 호출과 같다. /status의 dispatch: 줄에도 이 세션의 이름이 나온다. 한 번에 50개까지 불러온다. 목록 머리의 수는 불러온 개수다.
  • 세션 header: 그 서버로 가는 모든 MCP 요청에 X-Dispatch-Client-Kind: evo-cli와 지금 세션의 X-Dispatch-Session-Id가 붙는다. 세션이 바뀌면 다음 요청부터 새 ID를 쓴다. "dispatch": true가 없는 서버에는 보내지 않는다.
  • 이름: dispatch_agent(작업 담당, 선택)와 dispatch_workspace(작업 공간, 없으면 작업 폴더 이름). 영문·숫자로 시작하고 영문·숫자·.·_·-만, 64자까지다. 설정한 값이 맞지 않으면 설정 오류로 시작하지 않는다. 폴더 이름은 맞지 않는 글자를 -로 바꿔 쓰고, 그래도 맞지 않으면(예: 한글만 있는 이름) 보내지 않는다. 주소 매칭은 대소문자를 구분한다.
  • 세션마다 이름(Issues #264, #266): evo --dispatch-name CD02는 그 실행의 이름(규격의 agent)을 정한다(dispatch_agent보다 우선, 설정 파일은 바꾸지 않음, 형식이 맞지 않으면 시작 전에 오류). TUI의 /dispatch name CD02는 실행 중에 바꾸고 바로 신호를 보낸다. /dispatch name은 지금 이름을, /dispatch name -는 이름을 지운다. 바꾼 이름은 /clear·/compact 뒤에도 남고 다시 실행하면 설정값으로 돌아간다. 이름을 바꿔도 세션 ID는 그대로이고 신호(heartbeat)만 보내므로 이미 받은 요청과 상태줄 수는 그대로다(서버는 받은 요청을 세션 ID로 묶는다). 새 요청만 새 이름을 따른다.
  • /dispatch list : 내 활성 세션 목록을 대화 화면에 한 번 출력한다(이 세션 포함, *·(this session) 표시). 줄마다 이름(없으면 no name) · 작업 공간 · TUI/-p · 클라이언트 종류(evo-cli가 아니면)이며 세션 ID는 보이지 않는다. 이름·작업 공간·모드가 같은 세션이 둘 이상이면 시작 시각(since HH:MM:SS, 같으면 #1·#2)을 덧붙여 구분한다(/dispatch send 목록도 같다). dispatch_sessions만 부르는 읽기 전용이다.
  • /dispatch send : 내 다른 활성 세션 목록(dispatch_sessions, 이 세션 제외: 이름 또는 no name · 작업 공간 · TUI/-p · 클라이언트 종류)을 열고, ↑/↓·Enter로 하나를 고른 뒤 입력창에 내용을 쓰면 evo가 모델 없이 그 세션 하나에만 보낸다(dispatch_send, to.kind: session). 첫 줄이 제목(200자까지), 나머지가 본문(16KB까지)이고, 한 줄이면 제목과 본문이 같다. 쓴 그대로 보내므로 비밀정보를 넣지 않는다. 보내기 전 Esc는 취소. 처음 요청을 세션 주소로 받는 것은 dispatch 서버가 허용해야 하며, 허용하지 않는 서버는 invalid_request로 거부한다(안내에 그대로 나온다). evo-dispatch는 develop 575b410(evo-dispatch #13)부터 허용한다.
  • 목록과 키 : /inbox·/dispatch send 목록은 불러오는 중에 다시 쳐도 한 번만 열린다. 목록이 왔을 때 실행 중이거나 승인 창 같은 다른 창이 열려 있으면 목록을 열지 않고 알림만 남긴다. 그때는 다시 친다. 승인 창은 늘 키를 먼저 받는다. /dispatch send로 세션을 고른 뒤 쓴 글은 실행 중이어도 Enter로 그 세션에 간다(이 세션 모델 대기열에 들어가지 않는다).
  • 받은 글의 제어 문자 : /inbox·/dispatch list·/dispatch send에 보이는 서버 글(제목·본문·링크·이름·작업 공간·오류)의 제어 문자는 \u001b처럼 글자로 보인다. 터미널 제어 문자열(클립보드 쓰기, 화면 지우기)은 실행되지 않는다. 모델에 붙이는 받은 요청 안의 </dispatch-request> 같은 닫는 태그는 <\/dispatch-request>로 바꿔, 요청 블록을 일찍 닫지 못한다.
  • 세션 특정하기: 처음 보내는 요청은 agent·workspace 이름으로 보내거나, 세션 하나를 골라 세션 주소(to.kind: session)로 보낸다(규격 v1 r4 보충 3부터). 보내는 쪽 에이전트는 dispatch_sessions로 활성 세션과 이름을 보고, 사용자가 말한 이름의 to.kind: agent(또는 workspace)로 보낸다. 사람이 직접 고를 때는 /dispatch send가 고른 세션 하나에 세션 주소로 보낸다. 답장은 세션 주소에 reply_to를 붙인다. 받는 세션의 이름은 그 세션의 /inbox 맨 위나 /status에 나온다. 이름은 클라이언트 종류와 관계없이 맞추지만, 같은 사용자(같은 로그인 발급자의 같은 계정)끼리만 주고받는다. 조직 로그인과 개인 로그인은 서로 다른 사용자다.
  • 기기 ID: 설치 단위로 한 번 만든 UUID를 ~/.evo/device-id(0600)에 둔다.
  • 보내지 않는 것: 작업 폴더 경로 원문(sha256 path_hash만 보낸다), 토큰 값(신호 본문·세션 기록에 없음). 로그인하지 않았으면 신호를 보내지 않고, 서버가 응답하지 않거나 오류를 내도 세션은 멈추지 않는다.
  • "dispatch"는 "auth": "org"와 함께만 쓸 수 있어서, auth처럼 사용자·managed 설정에서만 허용된다.

evo-dispatch 버그 신고 공간 (/mcp/bugs)

섹션 제목: “evo-dispatch 버그 신고 공간 (/mcp/bugs)”

evo를 쓰다 생긴 버그를 evo-dispatch의 버그 신고 공간에 올리고, 내가 올린 버그의 처리 상태를 본다. 위의 /mcp(내 세션끼리 요청 주고받기)와 별개다. 올린 사람은 자기 버그만 보고, 처리하는 쪽에는 올린 사람 신원이 보이지 않는다.

  • 기본 등록: 개인용 빌드(조직 키 파일 없음)는 빌드에 넣은 dispatch 주소가 /mcp로 끝나면 같은 서버의 /mcp/bugs를 dispatch-bugs(auth: "org", "dispatch": true 아님, 연결 상한 3초)로 dispatch 뒤에 붙인다. 설정 파일에는 쓰지 않는다. TUI·-p 모두 뒤에서 연결해 시작을 기다리게 하지 않는다(-p는 연결된 뒤 요청부터 그 도구가 붙는다).
  • 사용자 설정에 auth: "org"이고 주소가 /mcp/bugs로 끝나는 서버("dispatch": true 아님)가 있으면 그것을 쓰고 기본 항목은 넣지 않는다. 이름만 dispatch-bugs인 다른 서버는 기본 항목이 그 이름을 대신 쓴다(위 dispatch와 같은 규칙). 직접 넣는다면 예: {"name": "dispatch-bugs", "url": "https://dispatch.example/mcp/bugs", "auth": "org"}.
  • 조직용 빌드·조직 모드(키 파일 dispatch.url로 dispatch를 기본 연결할 때도)는 기본 등록하지 않는다. 개발 요청 공간(/mcp/dev, 옛 /mcp/board)은 어느 빌드도 등록하지 않는다. 옛 /board 명령은 없앴다. 사용자 설정에 직접 넣은 옛 주소 항목은 evo가 지우지 않는다.
  • 쓰는 방법: 모델에게 말하면 도구를 부른다.
    • “evo 버그 신고해줘” → bug_report(product, title, body: 증상·재현 순서·기대한 동작). product는 분류다: CLI(Evo CLI, 기본), CHAT(웹 채팅), EMULATOR(에뮬레이터), MAC·WINDOWS(앱), DISPATCH(dispatch). 모델이 비워 두면 evo가 CLI를 넣는다. 정확한 목록은 서버 입력 형식이 정한다.
    • “내가 올린 버그 어떻게 됐어?” → bug_mine(최근 순, 상태 open 접수·taken 확인 중·closed 처리 끝과 처리 메모)
  • CLI 버그의 version·os가 비어 있으면 evo가 채운다(evo 버전, linux/amd64처럼 운영체제/아키텍처). 다른 분류에는 넣지 않는다. 모델이 넣은 값은 그대로 둔다. auth: "org"이고 주소가 /mcp/bugs로 끝나는 서버에만 한다.
  • 화면 캡처 붙이기: “이 화면도 같이 올려줘”처럼 말하면 모델이 images에 이미지 파일 경로({"path": "shot.png"}, 상대 경로는 작업 폴더 기준, ~ 가능)나 이 대화에 붙인 이미지({"image": "[Image #1]"}, @shot.png·붙여넣기·Ctrl+V로 보낸 것, -p는 프롬프트의 @이미지)를 넣고, evo가 서버 형식(mime_type·base64 data)으로 바꿔 보낸다. 최대 3장. PNG·JPEG·WebP이고 1MiB 이하면 그대로 보내고, 크거나 다른 형식(GIF 등)이면 긴 변 1600px 이하 JPEG로 줄여 1MiB 이하로 만든다. 3장을 넘거나, 파일이 없거나, 이미지가 아니거나, Read 거부 규칙에 걸린 경로면 아무것도 보내지 않고 도구 오류로 모델에게 알린다. 이미 data가 있는 항목은 그대로 둔다. 세션 기록에는 모델이 준 경로·토큰만 남고 이미지 데이터는 남지 않는다. 모델에게 보이는 bug_report 입력 형식에 이 두 방법을 짧게 덧붙인다. 보내기 전에 화면에 비밀정보·다른 사람 개인정보가 찍히지 않았는지 확인한다.
  • 본문에 비밀정보(토큰·비밀번호·내부 주소·개인정보)를 넣지 않는다.
  • 붙여넣은 텍스트는 줄바꿈을 그대로 살려 입력창에 들어가고, 전송되지 않는다. Enter를 눌러야 보낸다.
  • CRLF·CR은 LF로, 탭은 공백 4칸으로 바뀐다. 이스케이프 시퀀스와 그 밖의 제어 문자는 지운다.
  • 10줄을 넘거나 1000자를 넘으면 입력창에 [Pasted text #1 +42 lines](또는 [Pasted text #2 1500 chars])로 접어 보인다. 보낼 때는 원문 전체가 전송되고, 대화 영역에도 원문이 나온다. 자리표시 글자를 고치면 원문과 연결이 끊겨 그 글자 그대로 보낸다.
  • 자리표시 바로 뒤에서 Backspace, 바로 앞에서 Delete를 누르면 자리표시 전체가 한 번에 지워지고 그 붙여넣기는 보내지 않는다. 이미지 표시 [Image #N]도 같다. 직접 친 같은 모양의 글자는 한 글자씩 지워진다.
  • 괄호형 붙여넣기(bracketed paste)를 보내지 않는 터미널에서는 붙여넣은 줄바꿈이 Enter로 들어온다. 그래서 글자 입력 직후 30ms 안에 온 Enter는 바로 처리하지 않고 30ms를 더 기다린다.
    • 그 사이 다른 입력이 오면 붙여넣기 중이라 보고 줄바꿈으로 처리한다.
    • 아무 입력도 오지 않으면 Enter로 처리해 전송한다. 한글 입력기는 조합 중인 마지막 글자와 Enter를 함께 보내는데, 이 경우가 여기에 해당한다.
    • 사람이 친 Enter는 앞 글자와 간격이 있어 기다리지 않고 바로 전송된다.
    • 끝이 줄바꿈인 붙여넣기는 마지막 줄바꿈에서 전송된다.

답변은 일반 밝기로, 아래 보조 정보는 흐리게(회색) 표시한다. Ctrl+O는 생각 과정과 도구 결과를 한꺼번에 펼치거나 접는다.

  • 생각 과정: ✻ Thought for 3s (ctrl+o to expand) 한 줄로 접는다.
  • 도구 묶음: 실행이 끝난 Bash·Read·Grep·Glob·WebFetch·WebSearch 호출이 답변 없이 이어지면 ⚙ Read 6 files, searched for 2 patterns (click or ctrl+o to expand) 한 줄로 접는다. 여러 파일을 한꺼번에 읽는 병렬 호출(호출이 먼저 모두 나오고 결과가 뒤에 오는 경우)도 접힌다. 오류(✗)가 있거나 실행 중인 마지막 묶음은 접지 않는다. 줄을 클릭하면 그 묶음만 펼친다.
  • 읽기·검색 결과(Read·Grep·Glob): 펼친 호출 아래에도 내용 없이 ✓ 46 lines (ctrl+o to expand) 한 줄만 보인다. 내용은 Ctrl+O로 본다.
  • 그 밖의 도구 결과(Bash 등): 앞 3줄과 … +N lines (ctrl+o to expand)를 보인다. 오류 결과도 같다. 파일 수정(Edit·Write) 호출은 접지 않는다.
  • 실행 요약 줄: run: 6.5s · 4 turns처럼 시간과 턴 수만 보인다. 토큰과 비용은 상태줄과 /cost에서 본다.

하위 에이전트(Agent 도구)가 도는 동안 입력창 위, 진행 줄(Working…) 위에 한 줄씩 보인다. 모두 끝나면 칸이 닫힌다.

● explorer · find the config · Read +1 · 12.3K tokens · 42s
● reviewer (bg) · check the diff · thinking · 3.1K tokens · 1m 5s
  • 순서: 이름(백그라운드 작업은 (bg)) · 하는 일(Agent 호출의 description 첫 줄, 없으면 생략) · 지금 하는 일 · 쓴 토큰 · 걸린 시간
  • 지금 하는 일: 마지막으로 부른 도구 이름이다. 여러 개를 한꺼번에 부르면 Read +1처럼 나머지 개수를 붙인다. 도구 결과가 돌아가 모델 답을 기다리는 동안은 thinking이다.
  • 토큰: 그 하위 실행의 모델 호출마다 입력과 출력 토큰을 더한 값이다(표기는 토큰 수 표기와 같다).
  • 걸린 시간: 시작부터 센다. 부모 실행이 끝나고 백그라운드 작업만 남아도 1초마다 바뀐다.
  • 병렬 Agent 호출(백그라운드 아님)과 백그라운드 작업이 모두 보인다. 4개를 넘으면 … N more로 줄인다.
  • 시작·끝 대화 줄(↗ background task …, ✓ task …)과 상태줄의 N tasks는 그대로다. headless(-p)·stream-json 출력과 세션 파일은 바뀌지 않는다.
  • 실행 중에 Enter로 보낸 메시지는 입력창 위 queued 목록에만 보인다(최대 3개 미리보기와 +N more). 모델에 들어가면 목록에서 빠지고 대화 영역에 사용자 메시지로 나온다.
  • 넣는 시점: 실행이 끝날 때까지 기다리지 않는다. 도구 실행이 끝나고 다음 모델 호출 직전에, 그때까지 쌓인 메시지를 전부 입력 순서대로 각각 사용자 메시지로 넣는다. 실행이 끝났는데 남은 메시지가 있으면 다음 실행으로 보낸다. 병렬로 처리하지 않는다.
  • 입력창이 비어 있을 때 ↑를 누르면 쌓인 메시지를 전부 한 줄씩 입력창으로 되돌린다. 고친 뒤 Enter를 누르면 한 메시지로 다시 쌓이고, 입력을 비우고 Enter를 누르면 버려진다.
  • Ctrl+X Ctrl+S는 쌓인 메시지와 입력 중인 내용을 합쳐 바로 보낸다(실행 중이면 멈추고 보낸다).
  • 실행 중 Esc·Ctrl+C는 실행을 멈춘다(interrupting… (ctrl+c again to exit)). 멈추기를 요청한 뒤에도 실행이 끝나지 않을 때 Ctrl+C·Ctrl+D를 다시 누르면 evo가 바로 끝난다. 멈추기를 요청한 뒤 3초 안에 돌아오지 않는 도구는 남겨 두고 실행을 끝낸다. 그 호출 결과에는 도구가 멈추지 않아 남겨 뒀다고 적힌다. 실행 중 /quit·/exit는 대기열에 넣지 않고 실행을 멈춘 뒤 바로 끈다. 끄기를 시작한 뒤 정리(원격 종료·LSP·MCP·메모리 저장)가 5초 안에 끝나지 않으면 evo가 종료 코드 1로 끝난다.
  • 도구가 자기 시간 제한(timeout_ms)보다 30초 넘게 돌아오지 않거나, 멈추기를 요청한 뒤 3초 안에 돌아오지 않으면 evo는 그 순간 evo가 기다리는 위치와 하위 프로세스 상태(이름·상태만, 명령줄 제외)를 세션 기록 옆 <세션 id>.stuck-<시각>.txt(같은 초에 또 남기면 -2, -3…이 붙는다)에 남기고 알림줄에 경로를 보여 준다. 문제를 알릴 때 이 파일을 함께 보내면 원인을 찾을 수 있다.
  • ! 셸 명령은 실행 중에 쌓을 수 없다.
  • ! 바로 뒤가 숫자이면(!131 병합 승인…처럼 GitLab MR 번호) 셸 명령이 아니라 보통 프롬프트로 모델에 간다.

입력 줄 맨 앞이나 띄어쓰기 뒤에 @를 치면 작업 폴더의 파일·폴더 목록(최대 8줄)이 입력창 위에 뜹니다. @ 뒤에 글자를 치면 목록이 좁혀집니다. 파일 이름이 그 글자로 시작하는 것이 먼저, 이름에 들어 있는 것, 폴더 경로에 들어 있는 것, 글자가 순서대로만 있는 것 순입니다.

  • ↑/↓, Ctrl+P/N: 고르기 · Tab/Enter: 넣기 · Esc: 닫기
  • 파일을 고르면 @경로 (뒤에 띄어쓰기)가 들어갑니다. 폴더는 @폴더/로 들어가고 목록이 그 안으로 바뀝니다.
  • 띄어쓰기가 있는 경로는 \ 로 적힙니다(@My\ Docs/a.txt).
  • a@b.c처럼 글자 바로 뒤의 @, /나 !로 시작하는 입력에서는 목록이 뜨지 않습니다.

목록은 git 저장소면 git ls-files(추적 파일과 아직 추가 안 한 파일, .gitignore 대상 제외)로, 아니면 폴더를 훑어(.git, node_modules, 맨 위 .gitignore 대상 제외) 만듭니다. 최대 60,000개(파일 5만 개 저장소까지 전부), 30초 동안 다시 씁니다. 처음 @를 칠 때 뒤에서 만들기 때문에 타자가 멈추지 않습니다. 그보다 많으면 가장 깊은 경로부터 빼고(이름 순서 뒤쪽이 빠지지 않음), 목록 맨 아래에 partial list: first 60000 paths, deepest left out 줄이 나옵니다. 이때 깊은 파일은 하위 폴더에서 evo를 열면 찾을 수 있습니다.

보내면 메시지 안의 @경로마다:

대상 보내는 것
텍스트 파일 내용(최대 256KB·2000줄, 한 줄 2000자). 잘리면 그 사실과 Read 도구 안내를 함께 보냄. 256KB에서 잘린 자리의 덜 끝난 글자만 버리고, 그 밖의 잘못된 UT은 바이너리로 본다. 내용 안의 </referenced-file> 같은 닫는 태그는 <\/…로 바꿔 블록을 닫지 못하게 한다
폴더 바로 아래 항목 이름(최대 50개), 내용은 보내지 않음
이미지(png 등) 지금처럼 이미지로 첨부
없는 경로 끝에 붙은 한글(조사)을 한 글자씩 떼며 다시 찾아 있으면 그 파일을 보냄(@main.go를 → main.go). 그래도 없으면 글자로 두고 @경로 not attached: not found 줄 표시. 있는 경로는 한글 이름이어도 그대로 씀
바이너리·작업 폴더 밖·.gitignore 대상·읽기 실패 글자로 두고 @경로 not attached: 이유 줄 표시
Read 거절 규칙(permissions.deny의 Read(…))에 맞는 파일·폴더 글자로 두고 @경로 not attached: denied by a Read rule 줄 표시. 폴더 목록에서도 맞는 항목은 뺌. 심볼릭 링크는 가리키는 파일로도 판정

@는 직접 친 글에서만 찾습니다. 붙여넣어 [Pasted text #N …]로 접힌 글 안의 @경로는 첨부하지 않고 글자 그대로 보냅니다.

대화 화면에는 친 글 그대로와 첨부마다 ⎿ @main.go (3 lines) 한 줄만 보입니다. 내용은 세션 파일에 남아 --resume·--continue 뒤에도 모델이 그대로 봅니다(화면은 같은 한 줄). 실행 중에 보내는 메시지(대기열)는 파일을 붙이지 않으므로, @파일이 든 메시지는 실행이 끝난 뒤 Enter로 보내라고 안내합니다. evo -p(화면 없이 실행)는 @를 풀지 않습니다.

비전 모델에 이미지를 보낼 수 있다.

  • @경로: 메시지에 @shot.png, @img/a.jpg, @~/Desktop/b.webp처럼 쓰면(작업 폴더 기준 상대 경로 가능) 그 이미지가 함께 간다. 글자에는 [Image #1]이 남는다. 확장자가 이미지가 아닌 @단어는 그대로 글자다. 끝에 붙은 한글(조사)은 파일 언급처럼 한 글자씩 떼며 다시 찾는다(@shot.png를 → shot.png, 글자에는 [Image #1]를). 그래도 없으면 조사를 뗀 이름으로 안내한다. 읽을 수 없는 이미지면 보내지 않고 입력을 남긴 채 안내한다. Read 거절 규칙에 맞는 이미지도 같다(denied by a Read rule, evo -p 포함).

  • 경로 붙여넣기·끌어다 놓기: 이미지 파일 경로 하나만 붙여넣으면(따옴표, \ 로 이스케이프한 공백, file:// 포함) 입력창에 [Image #N]으로 들어간다. Read 거절 규칙에 맞는 파일이면 첨부하지 않고 알린다. code-server처럼 원격 터미널에서는 이미지를 VS Code 탐색기로 끌어다 올린 뒤 @경로로 쓴다.

  • Ctrl+V: 클립보드에 이미지가 있으면 [Image #N]으로 첨부한다. 없으면 지금처럼 글자를 붙여넣는다. macOS는 osascript, Linux는 wl-paste(Wayland) 또는 xclip(X11)로 읽는다. 맥에서는 Cmd+V가 아니라 Ctrl+V를 쓴다(Cmd+V는 터미널이 글자만 붙여넣는다). SSH나 code-server 같은 원격 세션은 사용자 PC의 클립보드를 읽을 수 없어 @경로를 쓰라는 안내만 나온다(다른 CLI도 같다). Windows는 아직 지원하지 않는다.

  • 대화 영역에는 ↳ image attached (png) 줄로 보인다. 실행 중에는 이미지 메시지를 대기열에 넣지 않는다(실행이 끝난 뒤 Enter).

  • -p: 프롬프트의 @경로도 같다. 읽을 수 없는 이미지면 사용법 오류(exit 2).

  • Read 도구: 에이전트가 이미지 파일(png·jpg·gif·webp)을 Read하면 도구 결과에는 [image: shot.png, 1280x720] 요약만 남고, 이미지 자체는 그 결과 바로 다음에 모델에게 보여 준다. 대화 영역에는 ↳ image shown to the model (png) 줄로 보인다. 이미지를 받지 않는 모델이면 [image omitted …] 글자로 바뀐다.

  • 형식: PNG·JPEG·GIF·WebP. 긴 변 2000px, 5MB를 넘으면 줄여서 PNG(그래도 크면 JPEG 품질 85)로 다시 만든다. 작은 이미지는 그대로 보낸다.

  • 전송: OpenAI 호환(LiteLLM 포함)은 image_url data URI, Anthropic은 base64 image 블록, Ollama는 메시지의 images로 보낸다.

  • 이미지를 받는 모델인지: 모델 행의 "vision": true|false가 있으면 그것을 따른다. 없으면 서버가 알려 준 값을 쓴다(모델 목록의 supports_vision·capabilities, Ollama /api/show의 capabilities — 문맥 크기를 알아낼 때 같은 응답에서 읽음). 둘 다 없으면 알 수 없음으로 보고 보낸다. 받지 않는 모델이면 이미지가 붙은 메시지는 보내지 않고 안내하며(-p는 오류), 모델을 바꾼 뒤 기록에 남은 이미지는 [image omitted: this model does not support images] 글자로 바꿔 보낸다. /status의 images: 줄에서 판단 결과를 본다.

  • 세션 기록에 준비된 이미지가 저장되어 --resume으로 이어 가도 다시 보낸다. Langfuse 기록과 대화 압축(요약)에는 이미지 대신 [image]만 들어간다. 문맥 크기 계산은 이미지 1장을 약 1600 토큰으로 친다.

  • 세션 파일은 줄 길이 제한 없이 읽는다. 큰 이미지 한 장이 4MiB를 넘는 한 줄이 되어도 다음 질문·--resume·--continue·--fork가 된다. 세션 파일을 읽지 못하면 상태줄 알림(ctx: session could not be read: …)이나 종료 때 한 줄(evo: session <ID> could not be read: …)로 알린다.

대화 영역은 마우스 휠, PgUp/PgDn, Home/End로 스크롤한다. 오른쪽 1열에 스크롤바가 있다.

  • 모양: 보이는 비율에 비례하는 thumb(█, 반 칸은 ▀/▄)와 흐린 트랙(│). 대화가 한 화면에 들어가면 그리지 않는다.
  • 스크롤바 열을 클릭하면 그 위치로 이동하고, 누른 채 끌면 따라 움직인다.
  • 위로 스크롤해 둔 동안 새 출력이 오면 상태줄 안내가 ↑ scrolled · new output · End returns로 바뀐다. End로 맨 아래로 돌아간다.
  • 위로 스크롤해 둔 동안 대화 영역 마지막 줄 오른쪽에 ↓ jump to latest (End) 버튼이 뜬다(새 출력이 있으면 ↓ new output · jump to latest (End)). 누르거나 End(맥은 fn+→)를 누르면 맨 아래로 돌아가고 버튼이 사라진다. 버튼은 그 줄 위에 겹쳐 그려 화면 배치가 바뀌지 않는다.
  • 텍스트 선택은 그냥 드래그다(아래 “드래그로 선택·복사”).

마우스가 켜진 기본 상태에서 대화 영역을 왼쪽 버튼으로 드래그하면 evo가 선택한 범위를 반전 표시한다. 버튼을 놓으면 선택한 글이 클립보드로 복사되고, 안내 줄에 copied N characters가 보인다.

  • 복사되는 글은 화면에 보이는 글자 그대로다. 색은 빠지고 줄 끝 공백은 지운다. 한글처럼 두 칸을 쓰는 글자는 반쪽으로 잘리지 않는다.
  • 화면 폭에 맞춰 나뉜 줄은 나뉜 그대로(줄바꿈 포함) 복사된다.
  • 드래그가 대화 영역 위·아래로 나가면 그 방향으로 한 줄씩 스크롤하며 선택을 넓힌다.
  • 움직이지 않고 누르고 떼면 클릭이다. 지금처럼 도구 묶음을 접고 편다.
  • 선택이 보이는 동안
    • Esc는 선택만 지운다. 진행 중인 응답은 멈추지 않는다.
    • Ctrl+C는 다시 복사하고 선택을 지운다. 종료·중단하지 않는다.
    • 다른 키는 선택을 지우고 평소대로 동작한다. 창 크기가 바뀌어도 지운다.
  • 복사 방법
    • 항상 OSC 52(터미널에 클립보드 쓰기를 맡기는 신호)를 보낸다. iTerm2·WezTerm·kitty·Ghostty·Windows Terminal·VS Code 계열 터미널이 받는다. iTerm2는 설정에서 허용해야 한다.
    • 클립보드 명령이 있으면 그 명령에도 넣는다. code-server 터미널은 code-server --stdin-to-clipboard, macOS는 pbcopy, Linux 데스크톱은 wl-copy·xclip·xsel이다. 명령은 셸 없이 실행하고 3초가 지나면 멈춘다.
    • 맥 기본 터미널(Terminal.app)은 OSC 52를 받지 않는다는 보고가 많다. 같은 맥에서 evo를 실행하면 pbcopy로 복사된다. SSH로 다른 서버에서 실행하면 복사되지 않으니 /mouse off로 터미널 자체 선택을 쓴다.
  • 대화 영역만 선택할 수 있다. 입력 줄·팝업·상태줄은 선택되지 않는다.
  • /mouse는 켜기·끄기를 바꾼다(또는 /mouse on·/mouse off). 선택은 ~/.evo/settings.json의 "ui": {"mouse": false}로 저장되고 다음 시작에도 적용된다. /config의 “마우스 사용”도 같은 설정이다. 기본값은 켜짐이다.
  • 켜짐: 마우스 휠로 대화를 스크롤하고, 스크롤바를 끌고, 클릭으로 도구 결과를 펼치고, 드래그로 선택·복사한다.
  • 꺼짐: evo가 마우스를 받지 않아 터미널이 드래그 선택·복사를 맡는다. 터미널의 클립보드 쓰기가 막혀 있을 때 쓴다. 스크롤은 PgUp/PgDn·Home/End로 한다. 도구 결과 펼치기는 Ctrl+O다.
  • 꺼진 동안 일부 터미널은 휠을 위·아래 화살표 키로 바꿔 보낸다. 그러면 휠이 대화 대신 입력 기록을 움직인다.
{"ui": {"scrollbar": "auto", "scrollSpeed": 3}}
설정 값 기본
ui.scrollbar auto: 맨 아래가 아니거나 방금(1.5초 안) 스크롤했을 때만 표시. 숨겨도 열은 비워 둔다 / always: 항상 표시 / never: 그리지 않고 열도 쓰지 않음 auto
ui.scrollSpeed 휠 1칸에 움직이는 줄 수, 정수 1~10(범위 밖은 기본값) 3

현재 값은 /config에 표시된다.

다른 CLI(Codex CLI·Gemini CLI·Claude Code)처럼 늘 보이는 곳은 토큰 수를 줄여 쓴다.

  • 줄임 규칙: 1,000 미만은 그대로(850), 그 위는 대문자 K·M·B. 단위 기준 100 미만은 소수 1자리, 그 위는 정수로 쓰고 끝의 .0은 지운다. 반올림한다(1,234 → 1.2K, 15,730 → 15.7K, 262,144 → 262K, 999,950 → 1M, 1,234,567 → 1.2M).
  • 줄여 쓰는 곳: 상태줄 ctx(12% 15.7K/262K, 창 크기를 모르면 15.7K tok), anthropic /model 목록(context 200K), /cost(1.2M in / 89K out tokens), goal 상태(tokens: 15.7K/250K, TUI·evo goal status).
  • 줄인 값과 정확한 수를 함께: /context·/status·/config의 창 크기·대화·지시문·자동 압축 시점(262K (262,144) tokens).
  • 정확한 수(쉼표): 헤드리스(-p) 텍스트 출력과 evo chat의 종료 요약(1,234,567 in / 89,012 out tokens).
  • 바뀌지 않는 것: --output-format json·stream-json의 정수, 상태줄의 비율(쓴 %), 비용(달러) 표기.

입력창 위에 뜨는 선택 팝업의 내용은 대화 기록에 남지 않는다. 팝업을 닫거나 고르면 사라지고, 결과는 입력창 위 안내 줄에 보인다.

  • /provider(인자 없음): 설정된 프로바이더 목록(주소·종류·키 출처·모델, *는 지금 모델), 지금 모델, add·use·key 사용법이 팝업 위쪽에 보이고 그 아래 종류 메뉴가 뜬다.
  • /provider와 첫 실행 설정의 모델 선택 단계: 찾은 모델 수는 안내 줄에 보인다(provider <주소> (<종류>): N models — …).
  • /model, 도구 승인, 폴더 신뢰 확인, yolo 확인, 설정 목록도 대화 기록에 쓰지 않는다.
  • /resume(인자 없음): 이 폴더의 세션 목록이 팝업으로 열린다(최근 순, 고친 시각 · 짧은 ID · 첫 질문, 지금 세션은 (this session)). 한 번에 8줄이고 ↑/↓로 움직이면 따라 넘어간다. Enter는 고른 세션으로 바꾸고(/resume <id>와 같다), Esc는 아무것도 바꾸지 않고 닫는다. 실행 중이거나 다른 선택 창이 열려 있으면 열지 않고 알린다. /resume <id>(앞부분만 써도 됨)는 목록 없이 바로 바꾼다.
  • 한 세션은 한 evo만 쓴다. 다른 evo(다른 터미널의 TUI나 -p)가 쓰고 있는 세션은 목록에 (in use)로 보이고, 고르거나 /resume <id>로 열면 지금 세션을 그대로 둔 채 이유를 알린다. 그 세션에 일을 맡기려면 그쪽 터미널을 쓰거나 /dispatch send로 보내고, 같은 대화에서 갈라 가려면 evo --fork <id>로 복사해 이어 간다. 잠금은 세션 파일 옆 <세션 ID>.lock의 OS 파일 잠금이라 evo가 끝나거나 죽으면 저절로 풀린다(빈 파일은 남는다). 잠금 파일을 만들 수 없는 곳에서는 잠금 없이 지금처럼 연다.
  • /provider add, /permissions처럼 명령의 결과로 보여 주는 출력은 팝업이 아니라 지금처럼 대화 기록에 남는다.

대화가 커지면 오래된 도구 출력을 정리하고, 그래도 크면 요약해서 대화를 줄인다(2단계). 시작 지점은 설정으로 정한다.

{
"autoCompact": {"enabled": true, "threshold": 0.85, "tokens": 120000},
"models": [{"id": "qwen3.8:27b", "auto_compact_tokens": 60000}]
}
  • 시작 지점 = context window를 알면 min(window × threshold, tokens), 모르면 tokens. 모델 행의 auto_compact_tokens가 전역 tokens보다 우선한다. 기본(설정 없음)은 window의 85%.
  • window도 tokens도 없으면 window를 128K(131,072)로 가정해 그 85%(약 111,411 토큰)에서 압축한다. /context에 가정한 값이라고 나오며, 모델 행에 context_window를 적으면 실제 크기를 쓴다. 조직·evo 로그인 모델은 LiteLLM 모델 목록의 크기를 쓴다. Ollama 모델에 context_window가 없으면 TUI와 headless 모두 Ollama 서버가 알려 주는 크기를 쓴다(headless는 시작할 때 최대 5초 기다리고, 답이 없으면 모르는 것으로 진행).
  • 모델이 “context가 너무 길다”는 오류를 내면 한 번 요약하고 같은 턴을 다시 시도한다(출력이 시작되기 전일 때만). 대화가 메시지 하나뿐이어도 동작한다.
  • 자동 압축 결과는 세션 파일에 compact 이벤트로 저장된다. 그래서 다음 질문과 --resume·/resume은 요약부터 이어 가고, 대화가 다시 기준을 넘을 때만 다시 압축한다. 화면 기록은 압축 전 대화도 그대로 보인다. 이 이벤트는 세션 파일에만 있고 --output-format stream-json에는 나오지 않는다.
  • "enabled": false면 자동 압축과 위 재시도를 끈다. 수동 /compact [지시]는 그대로 쓸 수 있다.
  • TUI: 자동 압축이 실패하면 알림을 보이고 실행은 계속한다. 3번 연속 실패하면 그 세션에서는 자동 압축을 멈춘다(/clear·/resume으로 초기화). headless(-p)는 압축 실패 시 지금처럼 exit 1로 끝난다.
  • 현재 시작 지점과 출처는 /context·/config에 보이고, 상태줄 ctx는 시작 지점의 90%를 넘으면 compact soon을 붙인다.
  • 상태줄 ctx는 마지막 모델 호출의 사용량(서버가 센 값)이다. 서버가 사용량을 주지 않으면 세션 파일로 센다. 세션을 이어 열거나(--continue·--resume·/resume) 모델을 바꾸면(/model·/provider) 화면을 먼저 보이고 ctx는 잠깐 뒤에 채운다.

TUI와 headless 모두 Final Report §4.5 순서로 지침 파일을 로드해 첫 user message로 주입한다:

~/.evo/EVO.md
→ <root>/AGENTS.md → <root>/EVO.md (있으면)
→ <CWD>/AGENTS.md → <CWD>/EVO.md (있으면) # root와 중복이면 첫 발생만
  • 파일당 32KiB cap (초과분은 잘림)
  • v1 import(@file) 없음
  • compact(수동/자동) 이후에도 보존된다

환경 블록과 기본 시스템 프롬프트

섹션 제목: “환경 블록과 기본 시스템 프롬프트”
  • 첫 user message의 맨 앞에 환경 블록을 싣는다. 지침 파일·자동 메모리 블록과 같은 자리이고, 시스템 프롬프트에는 넣지 않는다(캐시 유지).
<env>
working directory: <CWD>
platform: <OS>/<arch>
date: <YYYY-MM-DD>
git branch: <브랜치> | none (detached HEAD at <커밋>) | git: not a git repository
</env>
  • git status와 디렉터리 트리는 넣지 않는다.
  • 날짜는 세션을 시작할 때의 값이다.
  • 기본 시스템 프롬프트(engine.DefaultSystem, 설정한 프롬프트가 없을 때)
    • 일반 지식·대화·환경 블록으로 답할 수 있으면 도구 없이 바로 답한다.
    • 파일·명령 결과·웹 내용이 필요할 때만 도구를 쓴다. 호출은 적게, 서로 관계없는 호출은 병렬로 한다.
    • 첫 메시지의 지침 파일·메모리는 다시 읽지 않는다.
    • 결과부터 짧게, 사용자의 언어로 답한다.
  • 근거: #259 측정(환경·저장소 질문의 도구 호출과 시간이 줄었다).
  • 하위 에이전트 중 자기 프롬프트가 없는 것은 새 기본 문구를 받는다. 환경 블록은 메인 대화(TUI·-p)에만 싣는다.

Auto memory (세션.digest · MEMORY.md 인덱스)

섹션 제목: “Auto memory (세션.digest · MEMORY.md 인덱스)”

세션이 끝날 때 그 세션의 요약 digest를 <config-dir>/projects/<path-slug>/memory/에 기록한다 (Final Report §4.3 예약 레이아웃 실현 — §4.5 지침 파일과 같은 첫 user message 슬롯으로 다음 세션에 주입된다):

<config-dir>/projects/<path-slug>/memory/
├── MEMORY.md # 인덱스 (최신 우선, 최대 200줄 + 32KiB, 초과분 trimmed 공지)
├── <session-id>.md # 세션 digest: 프롬프트(최대 12개) · tool 집계(Bash×3 …) · 편집 파일 · 결과/비용
  • <config-dir>: EVO_CONFIG_DIR가 없으면 ~/.evo(범위는 “저장 위치와 EVO_CONFIG_DIR” 절). <path-slug>는 프로젝트 cwd의 sanitize된 slug (세션 트랜스크립트와 같은 트리)
  • flush 시점: headless run 종료, TUI 종료(/exit·Ctrl-C/D), /clear, /resume으로 세션 전환, compact 이후 — 실패해도 실행은 중단되지 않는다 (fail-open, stderr/공지로만 보고)
  • user prompt가 없었던 세션은 아무것도 쓰지 않는다
  • 질문 하나에 도구 없이 답하고 끝난 세션(인사, 간단한 질문 등)도 쓰지 않는다. 도구를 썼거나 질문이 둘 이상이면 쓴다.
  • 새 세션의 첫 메시지에는 색인의 최근 20줄만 싣는다. 더 오래된 세션이 있으면 그 개수와 digest 폴더를 한 줄로 알리고, 모델은 필요할 때 그 폴더의 파일을 읽는다. 디스크의 MEMORY.md는 지금처럼 200줄 + 32KiB까지 유지한다.
    • 근거: #290 측정. 색인이 상한까지 차면 첫 메시지가 약 12천 토큰 늘었다. 20줄이면 약 1.8천 토큰이다.
  • digest가 없는 프로젝트의 첫 세션은 인덱스 없이 시작한다 (MEMORY.md 없음 = 오류 아님)
  • 지침 파일은 트랜스크립트에 저장되지 않는 request 전용 메시지이므로 digest가 자기 인덱스를 되먹이지 않는다 (self-feed 없음)
  • 설정: "auto_memory": { "enabled": true } (기본 on) — off면 로드/플러시 모두 no-op
  • /memory가 디렉터리·digest 수·인덱스 경로를 함께 표시한다

Auto permission classifier (/permissions recommend)

섹션 제목: “Auto permission classifier (/permissions recommend)”

모델 테이블의 모델로 tool 없는 사이드 콜(compact 요약기와 동일한 패턴)을 수행해 최소 권한 권한 모드를 추천한다 (Human Decision: 추천만, 자동 적용 금지):

/permissions recommend # 추천 표시만 — 모드는 바뀌지 않는다
/permissions apply # 사용자가 명시적으로 승인할 때만 세션 모드 변경
/permissions dismiss # 추천 폐기
  • 추천 후보는 plan · acceptEdits · default 셋뿐 — bypassPermissions는 추천 대상이 아니다. 후보 밖 답변(예: bypass)은 에러로 처리되고 아무것도 적용되지 않는다 (fail-closed)
  • 적용은 세션 한정 (this session only) — classifier 추천은 어떤 파일에도 영구 저장되지 않는다. sandbox는 매 Bash 호출 시점에 현재 모드로 재해석된다
  • 모델 슬롯: "auto_permission": { "model": "<model-table id>" } (미설정이면 실행 모델)

시작 권한 모드 설정 (permissionMode)

섹션 제목: “시작 권한 모드 설정 (permissionMode)”

시작할 때의 권한 모드를 설정 파일에 정해 둘 수 있다.

{ "permissionMode": "acceptEdits" }
  • 값은 default · acceptEdits · plan · auto 중 하나다(대소문자·-·_·공백 무시, 화면 이름도 허용 — 예: "edits mode"). 플래그·설정이 모두 없으면 대화형 TUI는 auto, 비대화형 -p는 default로 시작한다. 기관이 TUI도 default로 시작하게 하려면 관리형 설정에 permissionMode: "default"를 둔다.
  • bypassPermissions는 설정으로 지정할 수 없다. 지정하면 시작 오류이며 --yolo로만 켠다(TUI는 시작할 때 확인 대화상자를 거친다).
  • 우선순위: 관리형 설정 > CLI 플래그(--permission-mode, --yolo) > 로컬 프로젝트 설정(.evo/config.local.json) > 사용자 설정(~/.evo/settings.json). 관리형이 값을 정하면 그 값이 최종이고, 다른 모드를 요구하는 플래그는 사용법 오류로 거부된다(같은 모드면 허용).
  • 공유 프로젝트 설정(.evo/config.json)에는 둘 수 없다 — 클론한 저장소가 모드를 바꾸지 못하게 신뢰 게이트가 거부한다.
  • 세션 중 /permissions·Shift+Tab으로 바꾼 모드는 저장되지 않는다. 현재 모드의 시작 출처(플래그/설정/기본)는 /status의 start mode: 줄에서 본다.
  • Shift+Tab 순환: default → edits → plan → auto → yolo → default (yolo 사용 여부는 사용자가 정한다). yolo에 들어갈 때는 /permissions와 같은 확인 대화상자를 거친다(3번으로 저장한 동의가 있으면 바로 전환, 2번은 이번만이라 다음 순환에서 다시 묻는다). 대화상자를 취소하면 evo를 끝내지 않고 default로 넘어간다. 관리형 설정이 yolo를 금지하면 순환에서 건너뛴다. 실행 중에는 모드를 바꾸지 않는다.

화면(상태줄·시작 배너·전환 알림·/permissions·/status·/config)에는 <이름> mode 형태의 화면 이름을, 설정·플래그·세션 파일에는 정규 값을 쓴다. 상태줄과 배너에는 모드 이름만 보이고 “권한” 같은 앞말은 붙이지 않는다.

화면 이름 정규 값 설명
default mode default 읽기 전용 작업은 자동, 파일 수정과 그 밖의 명령 실행은 매번 묻는다
edits mode acceptEdits 작업 폴더 안 파일 수정은 묻지 않는다. 그 밖의 명령 실행은 계속 묻는다
plan mode plan 읽기만 한다. 파일 수정과 읽기 전용이 아닌 명령 실행은 거부된다(ls·cat 같은 읽기 전용 명령은 default처럼 허용)
auto mode auto edits mode에 더해, 규칙이 정하지 못한 명령은 분류 모델이 허용·차단을 판단한다 (아래 “auto 권한 모드”)
⚠ yolo mode bypassPermissions 모든 도구를 묻지 않고 실행한다. 샌드박스도 꺼진다 (-y·--yolo)

/permissions <모드>와 --permission-mode는 대소문자와 -·_·공백을 무시하므로 화면 이름(edits mode), 정규 값(acceptEdits), accept-edits 같은 철자를 모두 받는다. 어떤 철자로 입력해도 저장되는 값은 정규 값이다.

보호 파일 : 에이전트가 할 수 있는 일을 넓히는 파일이다. 이 파일에 대한 Write·Edit은 edits mode·auto mode·allow 규칙·“이 세션 허용”이 있어도 항상 묻는다. yolo mode와 plan mode는 각자의 판정을 따른다.

  • .evo/ 바로 아래의 config.json, config.local.json, settings.json, trusted_folders.json. 프로젝트·하위 폴더·홈(~/.evo/) 모두 해당한다.
  • .evo/agents/ 아래의 모든 파일(서브에이전트 정의).
  • 경로는 정리(.., ./)하고 심볼릭 링크를 따라간 뒤 판정한다. 확인할 수 없는 경로는 보호 파일로 본다.
  • 샌드박스 안의 Bash도 <작업 폴더>/.evo에는 쓸 수 없다. Linux는 .evo를 읽기 전용으로 묶고, 폴더가 없으면 빈 .evo를 먼저 만들어 묶는다(작업 폴더가 쓰기 가능한 모드일 때). macOS는 폴더가 없어도 쓰기를 막는다. .evo가 가리키는 곳이 없는 심볼릭 링크면 보호할 수 없어 Bash를 실행하지 않는다. 샌드박스가 없는 yolo mode와 Windows에는 해당하지 않는다.

규칙과 모드가 정하지 못한 도구 호출은 입력창 위의 테두리 창으로 묻는다. 창 위에 빈 줄이 하나 있다. 창 안에는 다음이 빈 줄로 나뉘어 있다.

  • 도구 이름
  • 실행할 명령(줄바꿈 그대로, 최대 6줄, 넘으면 남은 줄 수 표시)
  • 묻는 이유
  • Do you want to proceed?와 선택지
  • 키 안내

세로 24줄보다 작은 화면에서는 명령을 2줄까지만 보여 준다.

선택지는 다른 CLI(Claude Code·Qwen Code·Gemini CLI·Codex)처럼 No가 맨 끝이고, 모든 줄을 번호로 고른다.

호출 선택지
Bash(명령 하나)·WebFetch·MCP 도구·WebSearch 같은 도구 전체 허용 1. Yes, once / 2. Yes, for this session — … / 3. Yes, always in this project — 규칙 / 4. Yes, always for this user — 규칙 / 5. No (esc)
파일 수정(Write·Edit·NotebookEdit), 저장할 규칙을 만들 수 없는 호출 1. Yes, once / 2. Yes, for this session — … / 3. No (esc)
ask 규칙·훅이 물은 호출, 세션 허용이 안 되는 명령 1. Yes, once / 2. No (esc)
  • 저장되는 규칙은 줄에 적힌 그대로이고, 세션 허용보다 넓지 않다. 만든 규칙이 바로 그 호출과 맞을 때만 3·4번을 보여 준다.
    • Bash: 명령 그대로(Bash(go test ./...)). &&·|로 이어진 명령과 *·?가 든 명령은 저장하지 않는다.
    • WebFetch: WebFetch(https://host/*). 포트나 사용자 정보가 있는 주소는 저장하지 않는다.
    • MCP 도구 등: 도구 이름.
    • 파일 수정은 다른 CLI처럼 세션까지만 허용한다.
  • 어디에 저장되나
    • 3은 <project-root>/.evo/config.local.json에 저장된다.
    • 4는 ~/.evo/settings.json의 permissions.allow에 더해진다(0600, 다른 키 보존, 중복 없음).
    • 고르면 이번 세션에도 바로 적용된다. 대화 영역에 always allowed … — saved to <파일> 줄이 남는다.
    • 파일이 깨져 있어 저장하지 못하면 이번 세션만 허용하고 saving failed 줄로 알린다. 깨진 파일은 고쳐 쓰지 않는다.
  • Bash 호출이 directory로 작업 폴더 밖에서 실행되면 allow 규칙·“이 세션 허용”과 상관없이 항상 묻는다(yolo mode 제외). 창에는 (in /etc)처럼 실행 폴더가 보이고, 세션 허용과 저장 줄은 나오지 않는다. 어느 폴더에서 실행해도 샌드박스가 쓰기를 허용하는 곳은 세션 작업 폴더와 임시 폴더뿐이다.
  • 승인 키
    • 승인은 번호 또는 ↑↓로 고른 뒤 Enter다. 글자 키(y·a 등)로는 승인하지 않는다.
    • 창이 열린 직후 0.5초, 그리고 글자를 입력하는 중(마지막 입력 후 0.7초 이내)에는 번호도 Enter도 승인으로 받지 않는다(Enter는 무시).
    • 창이 열린 동안 입력한 글자(숫자 포함, 위 조건일 때)는 사라지지 않고 입력창에 남는다. 메시지를 치다가 창이 떠도 실수로 승인되지 않는다.
  • 거부 키
    • 거부는 맨 끝 번호·n·Esc·Ctrl+C다.
    • 글자를 입력하는 중에는 번호와 n도 입력창 글자가 된다. 그때도 Esc·Ctrl+C는 바로 거부한다.

--permission-mode auto, /permissions auto, Shift+Tab, 설정 permissionMode: "auto"로 켠다. 플래그·설정이 없으면 대화형 TUI는 auto mode로 시작한다. -p의 기본 시작 모드는 default 그대로다.

  • 판정 순서는 바뀌지 않는다. deny 규칙 → ask 규칙 → 권한 설정 파일 보호(#89) → 모드(edits mode와 같음: 읽기 전용, 읽기 전용 셸, 작업 폴더 안 편집은 허용) → 이 세션 허용 → 허용 규칙. 여기까지 정해지지 않아 사람에게 물었을 호출만 분류기가 판단한다. 분류기는 deny·ask 규칙과 보호 파일을 뒤집지 못한다.
  • 분류기: 도구 없는 모델 호출 1번이다(서브에이전트가 아니다). 모델은 auto_permission.model, 없으면 세션 모델이다. 특정 모델에 묶이지 않는다. 출력 한도 2048, 호출당 60초.
  • 분류기가 보는 것: 사용자가 직접 입력한 메시지(실행 중 대기열 메시지와 /goal set의 목표 포함), 에이전트의 이전 도구 호출, 판정할 호출. 도구 결과·어시스턴트 텍스트·지침/메모리 파일·작업 알림은 넣지 않는다(파일이나 웹 내용에 심은 문구에 속지 않도록). 서브에이전트의 호출도 부모 세션의 사용자 메시지로 판단한다.
  • 허용 기억: 분류기가 허용한 호출은 이 세션에서 기억한다. 같은 도구에 입력이 완전히 같으면 다시 묻지 않는다. 차단과 오류는 기억하지 않는다. 작업 폴더 밖에서 실행하거나 폴더 밖 경로를 쓰는 호출, 서브에이전트의 호출은 매번 묻는다. 세션이 바뀌거나 권한 규칙이 바뀌거나 auto로 다시 들어오면 기억을 비운다. 사용자가 새 메시지를 보낼 때도 비운다. 새 말을 보고 다시 판단하게 하려는 것이다.
  • 차단: 실행하지 않고, 에이전트에게 사유와 함께 “우회하지 말고 필요하면 사용자에게 알리라”는 결과를 돌려준다.
  • 분류기 오류(호출 실패·시간 초과·빈 답변·형식 오류): 차단이 아니라 사람에게 묻는다. 비대화형(-p)은 거절한다. 오류는 차단 횟수에 들어가지 않는다.
  • 차단 한도: 연속 3회 또는 세션 누적 20회 차단되면 그 세션의 auto를 끄고 default mode(default)로 돌아간다. /permissions auto로 다시 켜면 횟수가 초기화된다. 한도는 상수이며 설정할 수 없다.
  • 주의할 점: 판단은 확률적이며 보안 경계가 아니다. OS 샌드박스를 쓸 수 없으면(시작 배너 sandbox off, /status) 분류기가 잘못 허용한 명령은 격리 없이 실행된다. Bash 전체 허용처럼 넓은 허용 규칙에 맞는 호출은 분류기를 거치지 않는다.
  • 알림: 대화형 TUI는 auto로 시작·전환할 때 대화창에 경고 줄을 넣지 않는다. 비대화형(-p)은 위 내용을 stderr에 한 번 알린다. 모드 전환 알림은 <모드 이름> — <한 줄 설명>만 보이고, sandbox 상태는 시작 배너·/status·/permissions에서 본다.
  • 샌드박스는 default와 같은 workspace-write다.
  • 한계: 판단은 모델마다 다르고 같은 모델도 반복하면 달라질 수 있다. 다른 모델은 보장하지 않는다. 되돌릴 수 없는 작업은 deny·ask 규칙으로 막아 두어야 한다. --resume으로 이어 간 세션은 이전 사용자 메시지를 분류기에 넘기지 않는다(새로 입력한 메시지부터 — 더 많이 차단하는 방향).
  • 판정은 권한 로그(아래 “권한 판정 기록”)에 source: classifier로 남는다.

Permission 규칙 영구 저장 (/permissions allow|ask|deny … [project])

섹션 제목: “Permission 규칙 영구 저장 (/permissions allow|ask|deny … [project])”
/permissions allow Bash(npm test) # 이번 세션에만 적용 (파일 없음)
/permissions allow Bash(npm run test:*) project # 로컬 레이어에 영구 저장
  • Read 규칙은 Grep·Glob에도 적용된다. Read(secrets/**) deny가 있으면 다음과 같다.
    • path가 secrets 또는 그 아래인 Grep·Glob 호출은 거부된다. Read ask 규칙이면 묻는다.
    • 상위 폴더(예: 프로젝트 루트) 검색은 그대로 실행된다. 대신 deny된 파일·폴더는 파일 목록과 내용 검색 결과에 나오지 않는다.
    • 한계: Bash로 읽는 명령(cat secrets/a, grep -r … .)에는 적용되지 않는다. 이런 명령은 Bash(...) deny 규칙이나 샌드박스로 막아야 한다.
  • Bash 규칙의 접두어:*는 접두어 자체와, 공백 또는 :로 이어지는 명령에 일치한다 (Bash(git log:*) → git log, git log --oneline; Bash(npm run test:*) → npm run test:unit). git logx처럼 단어 중간에서 이어지는 명령에는 일치하지 않는다
  • project 토큰이 붙은 규칙만 <project-root>/.evo/config.local.json(0600)의 permissions.allow|ask|deny 배열에 추가된다 — 다른 키는 보존되고, 파일이 없으면 .evo/ 디렉터리부터 생성하며, 같은 규칙의 재추가(idempotent)는 무시된다
  • git 공유 레이어 .evo/config.json에는 절대 저장하지 않는다: permissions는 ProtectedRoots 키이므로 공유 레이어에 있으면 다음 config.Load가 trusted gate에서 거부한다 (클론받은 저장소가 agent 행위를 사전 승인하면 안 된다)
  • 세션 규칙은 live 게이트에만 반영되고 종료와 함께 사라진다; 영구 규칙은 다음 시작 시 config.Load가 로드한다
  • 설정을 합칠 때 permissions.allow·ask·deny 목록은 위 단계가 아래 단계를 덮지 않고 이어 붙는다(사용자 설정 → 프로젝트 → 로컬 → 명령줄, 같은 규칙은 한 번). 그래서 사용자 설정에 저장한 규칙은 로컬 파일에 자기 목록이 있는 프로젝트에서도 적용된다. 조직(managed) 설정은 지금처럼 마지막에 이긴다

에이전트가 파일을 편집하면(Write/Edit tool) 해당 언어의 LSP 서버가 diagnostics를 발행하고, evo는 이를 두 경로로 소비한다: TUI 대화 흐름에 ⚠ file:line: message notice 행 표시, 그리고 다음 모델 호출 직전 턴 경계에 [lsp diagnostics] … 사용자 메시지로 컨텍스트 주입(headless 포함 — 같은 engine 배선). 서버 시작 실패(바이너리 없음·핸드셰이크 실패·충돌)는 경고 notice 1회 + 해당 언어의 세션 잔여 동안 비활성화일 뿐, 실행 루프에 영향을 주지 않는다.

턴 경계에서는 서버의 답을 모두 합쳐 최대 2초까지 기다린다. 파일 하나는 최대 1.5초 기다린다. 그보다 늦게 온 diagnostics는 다음 턴 경계에 붙는다.

내장 프리셋 (Human Decision 2026-10-01: 6개 언어)

섹션 제목: “내장 프리셋 (Human Decision 2026-10-01: 6개 언어)”

편집된 파일의 확장자로 언어를 매기고, PATH에 서버 바이너리가 있을 때만 lazy 시작한다(사용하지 않는 언어는 프로세스를 띄우지 않는다). 바이너리 설치/업그레이드는 사용자 소관이다.

언어 프리셋 명령 확장자 관례상 서버
Go gopls .go go install golang.org/x/tools/gopls@latest
TypeScript/JavaScript typescript-language-server --stdio .ts .tsx .js .jsx .mjs .cjs npm i -g typescript typescript-language-server
Python pylsp .py .pyi pip install python-lsp-server
Rust rust-analyzer .rs rustup component
C/C++ clangd .c .h .cpp .cc .cxx .hpp .hh LLVM 배포
Java jdtls .java eclipse.jdt.ls

config 오버라이드 (프로젝트 .evo/config.json)

섹션 제목: “config 오버라이드 (프로젝트 .evo/config.json)”

lsp 키로 프리셋을 덮어쓰거나 새 언어를 추가/비활성화한다(lsp는 ProtectedRoots가 아니므로 공유 프로젝트 레이어에 둘 수 있다). 형태가 잘못되면 hooks/MCP와 동일하게 시작 시 에러로 실패한다.

{
"lsp": {
"go": { "command": "/opt/local/bin/gopls", "args": ["-v"] },
"mylang": { "command": "ml-lsp", "extensions": [".ml"] },
"python": { "enabled": false }
}
}
  • 항목 필드(전부 선택): command, args, env(객체), extensions(프리셋 확장자 덮어씀), startupTimeoutMs(기본 10000), enabled(기본 true)
  • 프리셋이 없는 언어는 command와 extensions가 필수다
  • "lsp": false — 기능 전체 끔(프리셋 포함)
  • lsp.*.command는 세션 중 편집 시점에 subprocess로 실행된다. 공유 프로젝트 레이어의 .evo/config.json에서 정의된 명령도 실행된다 — hooks와 동일한 위험 계급/동일한 posture다(클론받은 저장소가 임의 명령을 사전 구성할 수 있으므로 신뢰할 수 있는 저장소에서만 사용). evo는 서버 바이너리를 설치하거나 관리하지 않는다
  • 서버 프로세스는 세션 종료 시 process-group kill로 정리된다; 통신은 stdio JSON-RPC(LSP base protocol)이며 읽기 프레임 상한을 넘는 행은 오류로 실패한다

Langfuse 트레이싱 — opt-in(기본 off). 켜면 evo의 wire 이벤트를 OTLP/HTTP JSON span으로 바꿔 {host}/api/public/otel/v1/traces에 배치로 보낸다(Basic 인증): 1 세션 = Langfuse session(모든 span에 langfuse.session.id·session.id), 1 run(프롬프트) = trace(root span 하나), 1 모델 호출 = root 아래 generation span(모델, 그 호출의 gen_ai.usage.input_tokens·output_tokens), 1 tool 실행 = root 아래 tool span(입/출력). Agent 도구로 실행한 하위 에이전트(백그라운드 포함)는 같은 trace에서 그 Agent tool span 아래 하위 트리다: 에이전트 이름의 agent span 아래 그 에이전트의 generation·tool span. 부모 실행이 먼저 끝나도 같은 trace에 붙는다. 1차 고객은 관리자/조직 관측(#37 HD)이며 개인의 사용량·비용 회고는 로컬 표면(result의 cost_usd, 상태줄, /cost, 세션 transcript)이 담당한다.

{
"telemetry": {
"enabled": true,
"langfuse": {
"host": "http://localhost:3000",
"public_key_env": "LANGFUSE_PUBLIC_KEY",
"secret_key_env": "LANGFUSE_SECRET_KEY"
}
}
}
  • 모든 필드 선택(기본값: host http://localhost:3000, env 이름 LANGFUSE_PUBLIC_KEY/LANGFUSE_SECRET_KEY, redact_tool_output true, max_output_chars 2000, send_prompts false, identity “plain”)
  • 키 값은 절대 config에 두지 않는다 — env 이름만 참조하고 실행 시 환경에서 읽는다. 키가 없으면 관측자가 만들어지지 않는다(외부 호출 0건)
  • "telemetry": false = 기능 off; 잘못된 형태는 시작 에러(hooks/MCP와 동일한 strict 파싱)
  • 저장소 공유 설정(.evo/config.json)에는 둘 수 없다. 클론한 저장소가 대화 기록을 보낼 곳을 정하지 못하게 하려는 것이다. 있으면 permissions·wrappers처럼 시작 오류로 멈춘다. ~/.evo/settings.json이나 .evo/config.local.json에 둔다.
  • trace의 userId는 git config user.email → OS 사용자명@hostname 폴백 체인으로 결정되고 trace metadata에 소스(git-email/os)가 함께 남는다
  • identity 옵션: plain(기본) / hash(sha256 — 우발적 노출 완화 수단일 뿐, 기업 이메일 목록에 대한 사전 역산에 취약함을 명시) / none(userId 미전송 — 기기·프로젝트 metadata만)
  • send_prompts: false(기본) — 프롬프트/응답 본문은 전송하지 않고 크기만. true면 trace 이름에 프롬프트 앞부분, 각 generation의 output에 모델 응답 텍스트가 들어간다(input은 크기 정보); tool 출력은 redact_tool_output true(기본)면 [redacted N chars], 어느 쪽이든 max_output_chars에서 절단
  • metadata(항상 부착): hostname, os, 프로젝트(cwd slug), session_id, identity_source
  • 관측자는 논블로킹 큐(256) 사이드 채널이다 — 네트워크 실패·큐 포화 시 조용히 drop하고 메인 루프에 영향이 없다(재시도 1회 후 drop, 재시도 루프 없음). 연결 실패·5xx·429만 재시도하고, 4xx와 일부 거부(partialSuccess.rejectedSpans, 오류 항목이 있는 207 응답)는 바로 실패로 친다
  • 배치는 주기(2s)·크기(20 span)·run 종료 시 전송된다
  • /doctor에 보낸·버린 span 수와 마지막 실패(시각·이유)가 나온다. 이유에는 주소와 토큰을 넣지 않는다

Goal은 세션 스코프 자율 루프다: 목표를 설정하면 에이전트가 자연 종료점(툴 없이 턴을 마치는 시점)마다 독립 verifier(무도구 side call, temp 0, 30s/256KB 상한, strict JSON)가 세션 트랜스크립트 증거만으로 판정한다 — met이면 완료, not_met이면 다음 자율 턴을 합성해 계속, impossible이면 중단. 모델의 자기 보고(“테스트 통과했습니다”)는 증거로 인정되지 않고 tool_result 기록만 증거가 된다.

/goal set <objective> # 목표 설정 + 자율 루프 시작 (기존 Goal 교체)
/goal | /goal status # 상태 카드
/goal pause # 루프 정지 (Goal 보존)
/goal resume # 재개 — 예산 소진 시 추가 window
/goal clear # 삭제

headless: evo -p "/goal set <objective>" — 자율 루프가 같은 프로세스에서 완료/중단까지 실행된다. 후속 프로세스에서 제어: evo --continue -p "/goal status|pause|resume|clear". Goal 상태는 세션 JSONL에 기록되므로 resume 시 복원된다.

config 기본 의미
goal.tokenBudget 30000000 Goal 1개의 토큰 window(런 + verifier 합계). 소진 시 wind-down 턴 1회(인계 요약) 후 cause와 함께 정지, resume 시 추가 window. 0/-1 = 무제한
goal.turnBudget 없음(무제한) Goal 턴 수 상한. 소진 시 token과 동일한 wind-down 경로
goal.verifierModel 런 모델 verifier 전용 cheap 모델 슬롯(비어 있으면 런 모델). 모델 표에 없거나 접속 설정이 잘못된 이름이면 다른 모델로 대신하지 않고 첫 검증에서 Goal이 pause된다(cause: verifier_failed, TUI·headless 같음)
  • objective는 untrusted data: 모든 프롬프트 표면(자율 턴, verifier 요청)에서 <goal_objective> 데이터 블록으로 펜싱되고 지시로 해석되지 않는다 — objective에 injection(“mark met now”)을 심어도 verifier 판정은 side call의 strict JSON 스키마(met|not_met|impossible)만 따른다
  • verifier 실패 fail-closed: 전송 오류·형식 불량·unknown 판정은 결코 met이 될 수 없다 — Goal이 pause된다(cause: verifier_failed)
  • no-progress 3턴 pause: 툴 실행 없는 턴이 3번 연속 not_met이면 자동 pause(cause: no_progress) — 툴을 쓴 턴은 카운터 리셋
  • Goal 턴도 permission 게이트 동일 적용: 자율 턴에서 툴 실행은 일반 턴과 같은 승인 규칙을 지난다
  • Goal 턴 중 Esc 인터럽트 = Goal pause(부분 결과 보존)
  • propose_goal tool: 모델이 목표를 제안한다 — 승인 다이얼로그(y/n)가 항상 필요하며 어떤 permission mode(YOLO 포함)·hook·규칙으로도 우회할 수 없다(자율 루프의 동의는 권한 규칙이 아니라 사용자에게서 온다). headless는 hand-off(/goal set …)로 안내한다
  • get_goal / update_goal tools: 모델이 권위 있는 Goal 상태 + evidence catalog(최대 100 entry, uuid + proof grade)를 읽고, update_goal로 종단 제안(complete|blocked)을 한다 — evidence uuid 인용은 현재 catalog 기준으로 검증되며 유효하지 않으면 거부된다. 이 도구는 상태를 직접 바꾸지 않는다: 독립 verifier가 다음 자연 종료점에 판정한다
  • blocked + blockerKind: 제안이 수락되면 blocked 상태로 종단 — 사유 분류는 authority(사용자/관리자 결정 필요) / external(외부 자원 불가) / repeated(동일 사유 3턴 반복 → 자동 blocked) / infeasible(목표 자체가 불가능 — external_fact 증거 필수)
  • checkpoint 압축: 25 goal 턴마다(또는 verifier window 64KB 도달 시) evidence catalog를 독립 compressor side call이 claims(≤32개·2,000자/개·16KB)로 압축하고 cursor를 진전시킨다 — 장기 Goal의 verifier 컨텍스트가 축소된다. 3연속 실패 시 Goal pause
  • active-time 예산: goal.activeTimeMinutes(기본 무제한, 0/-1 = 무제한) — 턴 사이 판독(절대 timer kill 아님). pause/blocked/프로세스 중단 시간은 미계산. 도달 시 wind-down 턴 1회 후 cause와 정지
  • config: goal.checkpointTimeoutSeconds(기본 180)

모델 테이블의 per-model pricing(pricing: {in, out} — USD/1M 토큰)으로 run 비용을 추정한다. result 이벤트의 cost_usd, --output-format json의 cost_usd, TUI의 /cost와 상태줄 누적 비용은 같은 계산((in×PriceIn + out×PriceOut)/1e6)을 따른다. pricing이 없으면 0이다.

claude wrapper(claude-cli)는 claude CLI가 알려 주는 추정 비용(total_cost_usd의 증가분)을 쓰고, 화면에는 ≈$0.0348처럼 ≈를 붙인다. API 정가 기준 추정이며, 구독으로 쓰면 실제로 청구되는 금액이 아니다. evo는 이 모델의 토큰에 단가를 따로 매기지 않으므로 두 번 계산되지 않는다.

이미 로그인해 둔 claude CLI를 evo의 모델처럼 고를 수 있다. claude는 자기 도구를 하나도 쓰지 않고 evo 도구만 쓴다. 그래서 명령 실행·파일 수정은 모두 evo의 권한 확인 창·훅·샌드박스를 거치고, 도구 행과 세션 기록도 평소와 같다.

  • 켜기: 기본은 꺼짐이다. /config의 claude wrapper(wrappers.claude)를 켜야 /provider 목록에 claude가 나온다. 끄면 다시 사라진다.
  • 켜는 동안: CLI 버전 확인과 Ollama 모델 목록 조회는 화면 밖에서 돈다. 화면은 멈추지 않고 <이름>: 확인 중…을 보인다. 행을 넣지 못하면(CLI 없음, 버전 범위 밖, Ollama 연결 안 됨) 이유를 알리고 설정을 켜지 않는다. /config에서 켠 것은 다시 꺼짐으로 돌린다. 결과가 실행 중이나 다른 창이 열려 있을 때 오면 모델 창을 열지 않고 알림만 남긴다. claude 로그인 확인도 같다(로그인 화면으로 넘기지 않음).
  • 설정 위치: wrappers(claude·copilot·codex 모두)는 사용자 설정(~/.evo/settings.json)과 직접 만든 커밋 안 된 .evo/config.local.json에서만 읽는다. 공유 프로젝트 설정 .evo/config.json에 있으면 신뢰 게이트가 시작을 거부한다. 저장소에서 온 config.local.json(git에 커밋됨, 또는 git이 아닌 폴더)의 wrappers는 신뢰와 상관없이 쓰지 않는다. 클론한 저장소가 사용자의 CLI(구독)를 모델로 띄우지 못하게 한다.
  • /provider 목록의 claude를 고르면 claude 로그인을 확인하고(claude auth status, 약 140ms, 화면은 멈추지 않음), 로그인돼 있으면 claude 모델 목록을 연다. 이 항목은 wrappers.claude가 이미 켜져 있을 때만 보이므로 설정은 그대로 켜 둔다. 로그인돼 있지 않으면 evo 화면을 잠시 비우고 claude 자신의 로그인(claude auth login)을 띄운다. 끝나면 evo로 돌아와 다시 확인한다. 실행 중 claude가 Not logged in이면 오류에 /provider에서 claude를 고르라는 안내가 붙는다. /config에서 켜면 claude-cli 행이 모델 목록에 들어가고 /provider의 claude에서 고른다. /model은 지금 쓰는 프로바이더의 모델만 보이므로, 다른 모델을 쓰는 중에는 /model에 나오지 않는다. 끄면 행이 사라진다. 사용자 설정(~/.evo/settings.json)이나 .evo/config.local.json에서만 켤 수 있다. 공유 프로젝트 설정(.evo/config.json)에 wrappers가 있으면 다른 모델 설정처럼 거부한다.
  • 개인용에서만 보인다. 조직 키 파일이 있으면(조직용·기관용) 조직 모델만 쓰므로 목록에도 /config에도 나오지 않는다. 개인용은 evo 로그인 뒤에 쓸 수 있다.
  • 조건: PATH에 claude가 있고 버전이 시험한 범위(2.1.305 이상 2.2.0 미만) 안이어야 한다. 아니면 시작할 때 이유를 알리고 추가하지 않는다. 확인은 켰을 때만 하며 약 12ms 걸린다.
  • 로그인: evo는 claude의 로그인 파일을 읽지 않고, claude 통신을 가로채지 않는다. 사용자가 평소 쓰는 claude를 그대로 실행한다.
  • 모델: claude CLI가 알려 주는 목록을 그대로 쓴다. claude를 stream-json으로 띄워 initialize 응답의 models(value·displayName·description)를 받는다. claude /model 화면과 같은 목록이고, 모델은 부르지 않는다. 이름과 설명은 바꾸지 않고 보여 준다.
    • 행: value가 default이면 claude-cli(--model 없음), 그 밖에는 claude-cli/<value>(--model <value>). 모델마다 claude 프로세스가 따로 뜬다.
    • 2026-10-22 실측(claude 2.1.305): 13개, 약 0.6초.
    • 계열별 최신만: claude 이름이 <계열> <버전>(예: Opus 5.5)이면 계열마다 가장 높은 버전만 보인다. 계열·버전은 claude가 준 이름에서 읽는다. 그 꼴이 아닌 이름(Default (recommended))은 남는다. 2026-10-22 목록 13개 → Default (recommended), Opus 5.5, Fable 5.1, Sonnet 5.5, Haiku 5.5. 저장 파일에는 전체 목록이 남는다.
    • claude는 전체 목록을 자기 원격 설정에서 받는다. 원격 설정·사용 통계를 끄는 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC·DISABLE_TELEMETRY·DISABLE_GROWTHBOOK 중 하나라도 있으면 기본 목록 5개만 준다. 그래서 목록을 받을 때만 이 셋을 빼고 띄운다(2026-10-22 사용자 결정). 대화할 때는 모두 그대로 끈다.
    • 저장: ~/.evo/claude-wrapper.json(0600)에 모델 값·이름·설명과 claude 버전(--version 출력, CLI 파일 표시와 함께)만 둔다. 계정 정보는 저장하지 않는다. CLI 파일이 같으면 시작 때 claude --version을 다시 실행하지 않는다(시작 약 9ms 줄임). 파일이 바뀌면 한 번 다시 확인한다.
    • 시작은 저장된 목록을 쓰고 claude를 띄우지 않는다. 목록이 없거나 하루가 지났거나 claude가 바뀌었으면 뒤에서 새로 받는다(다음 시작부터 반영). /provider → claude는 로그인 확인과 함께 바로 받는다. 저장된 목록이 없으면 claude-cli 한 행만 있다.
  • 모델 목록: /model은 지금 쓰는 프로바이더의 모델만 보여 준다. claude wrapper를 쓰는 중이면 claude 행만, copilot이면 copilot 행만, codex면 codex 행만, evo면 evo 모델만 나온다. 다른 프로바이더로는 /provider에서 옮긴다.
  • 실행 방식: claude -p --input-format stream-json --output-format stream-json --verbose --include-partial-messages --tools "" --setting-sources "" --strict-mcp-config --no-session-persistence --disable-slash-commands --mcp-config <파일> --allowedTools mcp__evo --permission-mode dontAsk --system-prompt-file <파일>. evo 도구는 127.0.0.1의 임시 MCP 서버로 넘기며, 실행마다 바뀌는 무작위 토큰이 없는 요청은 거부한다. 토큰과 시스템 프롬프트는 0600 임시 파일로 넘기고 명령줄에 쓰지 않으며, 끝나면 지운다. 긴 도구 실행(승인 대기·긴 빌드)이 claude 쪽에서 시간 초과되거나 배경 작업으로 넘어가지 않게 MCP_TOOL_TIMEOUT·CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT=0·CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS=0을 넣는다.
  • 모델 역할만: claude는 evo 도구 호출을 제안하고 답을 쓰는 일만 한다. 다른 프로그램을 실행하지 못하게 PATH를 빈 임시 폴더로 바꾼다(macOS는 키체인 로그인을 읽는 security만, Windows는 시스템 폴더만). 부가 통신·원격 측정·오류 보고·자동 업데이트·부가 모델 호출·claude 자체 압축·스킬·명령은 환경 변수와 --disable-slash-commands로 끈다. 저장소의 .claude/settings*.json·.mcp.json·AGENTS.md·CLAUDE.md에 든 명령·훅·MCP·지시는 실행되거나 모델에게 들어가지 않는다(미끼 저장소 strace 실측). 끌 수 없어서 남는 일은 claude 자신의 정리 작업뿐이다: 작업 폴더의 파일 이름 목록·git 메타데이터·설정 파일 읽기(모델에게는 작업 폴더 경로만 들어간다), 자기 세션 목록·MCP 연결 로그(~/.claude, ~/.cache/claude-cli-nodejs) 쓰기.
  • 대화: 한 대화에서는 claude 프로세스 하나를 계속 쓴다. 재개·압축 뒤처럼 대화가 맞지 않으면 새 프로세스를 띄우고 이전 대화를 글로 넘긴다(블록마다 4,000바이트까지). evo가 끝나면 프로세스를 모두 끝낸다(프로세스 그룹 종료).
  • 답하는 중 Esc: 래퍼 셋 모두 CLI를 끄지 않고 CLI 자신의 중단을 쓴다. claude는 stream-json control_request(interrupt), codex는 turn/interrupt(스레드·턴 id), copilot은 session.abort. evo 쪽 Esc는 기다리지 않고 바로 끝나고(가짜 CLI 측정 cancel→Done 중앙값 claude 1.9ms→0.004ms, codex 0.11ms→0.002ms), CLI가 턴을 끝냈다고 알릴 때까지(claude는 중단 응답과 result, codex는 turn/completed, copilot은 idle) 뒤에서 읽는다. 그동안 온 다음 질문은 그 확인을 기다린다. 확인되면 같은 프로세스가 대화를 그대로 들고 있어 다음 질문은 새 입력만 보낸다(다시 띄우지 않고 이전 대화를 글로 넘기지 않음, claude 프롬프트 캐시 유지). 멈춘 턴에서 evo에 넘기지 못한 도구 호출은 오류로 답한다. 2초 안에 확인이 없거나 CLI가 죽거나 중단을 거부하면 예전처럼 끄고 다음 질문은 새로 띄운다. 확인(2026-10-27, 실제 CLI, 답이 흐르기 시작하면 Esc): claude 2.1.310·codex 0.142.4·copilot 1.0.80 모두 같은 세션이 남고(중단 확인까지 각 10ms·6ms·21ms) 다음 질문에서 앞 요청을 기억했다. claude는 답하는 중 중단에 control_response success와 result(error_during_execution)를 주고, 쉬는 중 중단에는 응답만 준다(result 없음).
  • 부가 호출(압축 요약·자동 분류기·goal 확인·다음 질문 제안)은 목록의 다른 모델로 보낸다. 다른 모델이 없으면 claude를 한 번 따로 실행한다.
  • 측정(2026-10-21, 같은 PC): 이어지는 질문 1.31.5초, 첫 질문 2.63.5초(claude 단독 실행 4.2~4.9초와 비슷하며 모델 응답 시간이 대부분), Esc 뒤 다음 질문 3.6초(다시 띄움, 전). 설정이 꺼져 있으면 시작·입력에 더해지는 일이 없다.
  • 한계: 이미지는 넘기지 않는다. claude 계정의 사용 한도가 그대로 적용된다.

GitHub Copilot CLI를 Ollama 모델로 돌려 evo의 모델처럼 고를 수 있다. GitHub 로그인은 쓰지 않는다. Copilot은 모델 역할만 한다: evo 도구 호출을 제안하고 답을 쓴다. 명령 실행·파일 수정은 모두 evo 엔진이 평소 경로(권한 확인 창·훅·샌드박스·도구 행·세션 기록)로 한다.

  • CLI가 멈추면: 대답을 기다리는 동안 2초마다 Copilot CLI에 살아 있는지 묻는다(ping, 살아 있으면 1ms 안팎). 프로세스가 끝났거나 10초 안에 답하지 않으면 copilot wrapper: Copilot CLI stopped로 끝내고, 다음 요청은 새 CLI를 띄운다.
  • 켜기: 기본은 꺼짐이다. /config의 copilot wrapper(wrappers.copilot)를 켜야 /provider 목록에 copilot이 나온다. 고르면 copilot/<모델> 목록이 열린다. 로그인은 필요 없다. 켜면 Ollama 서버의 모델마다 copilot/<모델> 행이 모델 목록에 들어가고(/provider의 copilot에서 고른다, /model은 copilot을 쓰는 중일 때만 이 행을 보인다), 끄면 사라진다. 설정 위치 제한은 claude wrapper와 같다(공유 프로젝트 설정 거부).
  • 개인용에서만 보인다. 조직 키 파일이 있으면 목록에도 /config에도 나오지 않는다.
  • Ollama 서버: OLLAMA_HOST가 있으면 그 주소, 없으면 모델 목록의 첫 Ollama 행(주소·키), 그것도 없으면 http://localhost:11434. 모델 목록은 그 서버의 /api/tags(ollama list와 같음)다.
  • 조건: PATH에 copilot이 있고 버전이 시험한 범위(1.0.80 이상 1.1.0 미만) 안이어야 하며, Ollama 서버에 닿아야 한다. 아니면 이유를 알리고 추가하지 않는다.
  • 시작 속도: copilot --version(약 0.4초)과 Ollama 목록은 ~/.evo/copilot-wrapper.json에 저장해 두고 다음 시작에 그대로 쓴다(0.1ms 이하). 버전은 copilot 실행 파일이 바뀌었을 때만 다시 확인하고, 목록은 시작한 뒤 뒤에서 새로 받아 다음 시작부터 쓴다. /config에서 켤 때는 바로 묻는다. 이 파일에는 주소·키·경로를 쓰지 않고 바뀌었는지 알기 위한 지문만 둔다. 저장된 목록이 없거나(첫 시작) 다른 Ollama 서버의 것이면, 시작 모델(기본 모델이나 --model)이 copilot·codex 모델일 때만 목록을 기다린다. 아니면 기다리지 않고 시작한다. TUI는 첫 화면 뒤에 목록을 받아 /model에 넣고, 받지 못하면 알린다(설정은 그대로). -p는 그 실행에 쓰지 않으므로 받지 않는다. codex wrapper도 같다.
  • 미리 띄우기: TUI가 켜질 때, 모델을 바꿀 때, MCP 서버가 붙을 때 지금 모델이 wrapper(claude·copilot·codex)면 그 CLI를 뒤에서 미리 띄워 둔다. 첫 질문은 그것을 바로 쓰고, 아직 뜨는 중이면 기다렸다가 쓴다. 측정(응답이 바로 오는 가짜 서버): 첫 질문 Enter→답이 copilot 530ms→84ms, codex 200ms→20ms. 도구나 시스템 프롬프트가 달라졌으면 미리 띄운 것은 쓰지 않고 지금처럼 새로 띄운다. wrapper마다 하나만 두고, 쓰이지 않으면 15분 뒤 다른 세션이 시작될 때 정리되며, evo가 끝나면 함께 끝난다. -p는 미리 띄우지 않는다.
  • 실행 방식: 공식 Copilot Go SDK(github.com/github/copilot-sdk/go)의 빈 모드(ModeEmpty)로 copilot --headless --stdio --no-auto-login을 띄운다. 시스템 프롬프트는 evo 것으로 바꾸고(replace), 도구는 evo가 선언한 것만 보인다(custom:*). Copilot이 도구를 부르면 그 답은 evo 엔진이 실행한 결과로 돌려준다(시간 제한 없음). 권한 질문은 모두 거절한다.
  • 모델 역할만: 오프라인 모드(COPILOT_OFFLINE, GitHub 통신 없음), 끝나면 지우는 임시 COPILOT_HOME, 아무것도 실행할 수 없는 PATH(Windows는 시스템 폴더만), GitHub 토큰·COPILOT_* 환경 변수 빼기. 세션에서는 안내 파일(AGENTS.md·.github/copilot-instructions.md 등)·파일 훅·git·스킬·세션 저장·원격 측정·설정 탐색·메모리·자체 압축을 끈다. 미끼 저장소 strace(2026-10-22): Copilot이 실행한 프로그램 0개, 저장소 파일 읽기 0개, 접속은 Ollama뿐, 쓰기는 임시 폴더와 Copilot 자기 패키지 잠금 파일(~/.cache/copilot/pkg/.../inuse.*.lock)뿐.
  • 저장소 AGENTS.md는 evo 자신이 프로젝트 안내로 넣으므로(다른 모델과 같다) 모델에게 보인다. Copilot이 따로 읽지는 않는다.
  • 대화: 한 대화에서는 Copilot 세션 하나를 이어 쓴다. 대화가 맞지 않거나(재개·압축 뒤) 도구 목록이 바뀌면 새로 띄우고 이전 대화를 글로 넘긴다. evo가 끝나면 프로세스와 임시 폴더를 정리한다.
  • 답하는 중 Esc: claude wrapper와 같다(session.abort 뒤 idle까지).
  • 부가 호출(압축 요약·자동 분류기·다음 질문 제안 등 도구 없는 호출)은 같은 Ollama 모델로 직접 보낸다.
  • 비용: Ollama 모델이므로 값을 매기지 않는다. 토큰 수는 Copilot이 알려 준 값을 상태줄·/cost에 쓴다.
  • 측정(2026-10-22, 같은 PC, Ollama 클라우드 모델): Copilot 시작 0.4초·세션 0.02초, 첫 질문(도구 1번) evo -p 3.4초, 이어지는 질문 0.65.9초(같은 모델 Ollama 직접 호출 0.71.9초, 흔들림은 대부분 모델 쪽), SDK가 들어가 배포 빌드 실행 파일이 38.1MB → 42.7MB, evo --version 시작이 13.1 → 13.7ms(설정과 상관없음). 설정이 꺼져 있으면 그 밖에 시작·입력에 더해지는 일이 없다.
  • 한계: 이미지는 넘기지 않는다. 컨텍스트 창 크기를 알 수 없어 표시하지 않는다. 새 Ollama 모델은 다음 시작부터(또는 스위치를 껐다 켜면) 보인다.

OpenAI Codex CLI를 Ollama 모델로 돌려 evo의 모델처럼 고를 수 있다. 시험용이며 나중에 정식 방식으로 바꾼다. OpenAI 로그인은 쓰지 않는다. Codex는 모델 역할만 한다: evo 도구 호출을 제안하고 답을 쓴다. 명령 실행·파일 수정은 모두 evo 엔진이 평소 경로(권한 확인 창·훅·샌드박스·도구 행·세션 기록)로 한다.

  • 켜기: 기본은 꺼짐이다. /config의 codex wrapper(wrappers.codex)를 켜야 /provider 목록에 codex가 나온다. 고르면 codex/<모델> 목록이 열린다. 개인용에서만 보이고, 설정 위치 제한·Ollama 서버 고르는 방법은 copilot wrapper와 같다.
  • 조건: PATH에 codex가 있고 버전이 시험한 범위(0.142.4 이상 0.143.0 미만) 안이어야 하며, Ollama 서버에 닿아야 한다. 아니면 이유를 알리고 추가하지 않는다. 버전과 모델 목록은 ~/.evo/codex-wrapper.json에 copilot과 같은 방식으로 저장한다(주소·키·경로 없이 지문만).
  • 실행 방식: codex app-server(stdio JSON-RPC)를 띄우고 evo가 고른 Ollama 모델을 evo 자신의 provider 항목(/v1, responses)으로 가리킨다. 스레드는 evo 시스템 프롬프트(baseInstructions)와 evo 도구(dynamic tools)로 만든다. Codex가 도구를 부르면 evo 엔진이 실행한 결과로 답한다. 승인·입력 요청 같은 다른 요청은 모두 거절한다.
  • 도구 이름: Codex는 mcp·mcp__* 이름을 예약해 받지 않으므로, evo의 MCP 도구(mcp__서버__도구)는 Codex에 evo_mcp__서버__도구로 알리고 돌아온 호출은 원래 이름으로 돌린다. 영문·숫자·_·- 밖의 글자는 _로, 128자를 넘거나 겹치면 끝에 지문을 붙인다.
  • 모델 역할만: 끝나면 지우는 임시 CODEX_HOME·HOME, 아무것도 실행할 수 없는 PATH, 환경 변수는 허용 목록만(, OPENAI_*·CODEX_*는 목록에 없음, agent-cli-wrap.md), 실행 환경 없음(environments: []), 읽기 전용 샌드박스, 승인 묻지 않음, 기록 남기지 않음(ephemeral, history 끔). Codex 기능은 codex features list에서 켜져 있는 것을 모두 끄고(셸·앱·플러그인·멀티 에이전트·훅 등), 추가 안내(권한·앱·협업 모드·환경 정보·프로젝트 문서·스킬)와 웹 검색·request_user_input도 끈다. 그래서 모델에 가는 도구는 evo 도구와 Codex의 update_plan(계획 메모)뿐이다. update_plan은 0.142.4 소스에서 조건 없이 들어가 끌 수 있는 설정을 찾지 못했다(사용자 결정 2026-10-22: 일단 이대로 적용). 부르면 evo는 결과만 받고 아무것도 실행하지 않는다.
  • strace(2026-10-22): Codex 프로세스의 접속은 Ollama뿐, 실행에 성공한 다른 프로그램 0개(getconf·lsb_release·tmux 시도는 모두 실패).
  • 대화·부가 호출·비용: copilot wrapper와 같다(답하는 중 Esc는 turn/interrupt). 한 대화에서 Codex 프로세스 하나를 이어 쓰고, 도구 목록이 바뀌면 새로 띄운다. 도구 없는 호출은 같은 Ollama 모델로 직접 보낸다. 토큰 수는 Codex가 알려 준 값을 쓴다.
  • 측정(2026-10-22, 같은 PC, Ollama 클라우드 모델): 앱 서버 시작(initialize) 약 0.15초, 도구 1번 쓰는 질문 1.3~3.6초. 새 의존성이 없어 실행 파일 크기는 그대로다.
  • 한계: 이미지는 넘기지 않는다. 컨텍스트 창 크기를 알 수 없어 표시하지 않는다.

/config는 사용자가 만지는 설정을 한 목록으로 연다(Claude Code /config 방식). 기존 읽기 전용 출력은 /config show.

  • 키: ↑/↓ 이동, Enter/Space 값 변경(토글·선택지 순환), ←/→ 숫자 증감, Esc 닫기. 변경은 즉시 ~/.evo/settings.json에 저장되며 별도 저장 단계가 없다(다른 키 보존, 파일 0600).
  • 항목(화면 순서): 시작 권한 모드(permissionMode, default/edits/plan/auto mode), Vim 모드(vimMode), 생각 과정 펼침(ui.thinking), 생각 모드(thinking.enabled)·생각 수준(thinking.effort), 업데이트 알림(ui.updateNotice), 시작 화면 로고 숨김(ui.hideBanner), 시작 화면 팁 숨김(ui.hideTips), 다음 질문 제안(ui.promptSuggestion), 스크롤바(ui.scrollbar), 마우스 사용(ui.mouse), 스크롤 속도(ui.scrollSpeed), 자동 컴팩션(autoCompact.enabled), 자동 메모리(auto_memory.enabled), claude wrapper(wrappers.claude)·copilot wrapper(wrappers.copilot)·codex wrapper(wrappers.codex) — wrapper 셋은 개인용에서만 보인다.
  • 생각 모드·생각 수준: 생각 모드(기본 켜짐)를 끄면 모델이 생각 없이 바로 답한다. Ollama(/api/chat)에는 think: false, OpenAI 호환(/chat/completions)에는 reasoning_effort: "none"을 보낸다. 생각 수준(기본 = 모델 기본, low·medium·high)을 고르면 같은 자리에 그 값을 보낸다. 기본값이면 아무것도 보내지 않아 예전과 같다. 서버가 이 값을 거부하면(생각을 못 하는 모델 등) 그 요청을 값 없이 한 번 다시 보내고, 그 모델에는 이번 실행 동안 보내지 않는다. 바꾸면 다음 모델 요청부터 쓰이고 서브에이전트도 같다. Anthropic 직접 연결은 evo가 생각을 켜지 않으므로 해당 없다. 래퍼는 각 CLI 방식으로 넘긴다: claude는 --effort <수준>(끄기는 Claude Code에 없어 가장 낮은 low), copilot은 세션의 reasoning effort(끄기 = none)를 주고 이때만 Ollama와 Responses API(/v1/responses)로 통신한다(Copilot 1.0.80은 chat completions로는 이 값을 보내지 않음), codex는 model_reasoning_effort와 함께 이 모델이 생각 수준을 받는다고 적은 모델 목록(model_catalog_json, 임시 폴더)을 준다(Codex 0.142.4는 목록에 없는 모델에는 보내지 않음). 목록을 받지 않는 Codex면 목록 없이 다시 띄운다. 기본값이면 세 래퍼 모두 예전과 같은 인자로 띄운다. 설정을 바꾸면 다음 요청에서 래퍼 프로세스를 새로 띄우고 이전 대화를 글로 넘긴다. 확인(2026-10-27, deepseek-v4.1-flash, 중계기 기록): copilot·codex 모두 기본은 값 없음, 끄기 reasoning.effort: "none", low "low", 도구 호출 정상. 측정(deepseek-v4.1-flash, -p 3번씩): 요청 한 번당 끝까지 중앙값 1.11초 → 0.84초. 모델에 따라 low는 효과가 없을 수 있다(deepseek-v4.1-flash는 끄기만 효과).
  • Vim 모드·생각 과정 펼침·마우스 사용·claude wrapper·copilot wrapper·codex wrapper는 즉시 적용되고 나머지 행에는 다음 시작부터가 붙는다.
  • 관리형·프로젝트 로컬·공유 프로젝트 설정이 값을 정한 항목은 잠김으로 표시되고 바꿀 수 없다(사용자 설정을 바꿔도 적용되지 않기 때문). 사유는 하단에 나온다.
  • 권한 규칙, 훅, MCP, 텔레메트리, 키·자격증명, yolo mode(bypassPermissions)는 목록에 없다. 이 화면은 사용자가 여는 UI이며 에이전트 도구가 아니다.

세션 로그(~/.evo/projects/*/*.jsonl)에 도구 호출마다 permission 줄이 한 줄씩 남는다. 설정은 없으며 항상 기록한다.

{"type":"permission","payload":{"tool":"Bash","decision":"ask","source":"default","response":"once"}}
  • decision: allow · deny · ask
  • source: rule(허용·물음·거절 규칙) · protected(권한 설정 파일 보호) · mode(모드가 정함) · session(이 세션 허용) · default(규칙·모드가 정하지 못해 물음) · hook(PreToolUse 훅) · classifier(auto 모드의 분류기가 허용·차단) · classifier_error(분류기가 판단하지 못해 사람에게 물음)
  • response(ask일 때만): once · session(이 세션 동안 허용) · denied · unavailable(물을 수 없는 실행이라 막힘)
  • 명령·경로·파일 내용·거절 사유는 기록하지 않는다. stream-json 출력에도 나오지 않는 로그 전용 줄이다.

승인율은 ask 가운데 사람이 답한 것(unavailable 제외)에서 once+session의 비율이다. 집계 예시(python3, 모든 프로젝트 합산):

import sys, json, glob, os, collections
c = collections.Counter()
for f in glob.glob(os.path.expanduser("~/.evo/projects/*/*.jsonl")):
for l in open(f):
try: e = json.loads(l)
except ValueError: continue
if e.get("type") == "permission":
p = e["payload"]; c[(p["tool"], p["decision"], p.get("response", ""))] += 1
for k, n in sorted(c.items()): print(n, *k)
ask = {r: sum(n for (t, d, r2), n in c.items() if d == "ask" and r2 == r) for r in ("once", "session", "denied")}
tot = sum(ask.values())
if tot: print("승인율 %.1f%% (%d/%d)" % (100 * (ask["once"] + ask["session"]) / tot, ask["once"] + ask["session"], tot))

이 줄이 생기기 전의 세션에는 기록이 없다.