trycompai/crm · 라이선스 MIT · 런타임 Bun 1.3.12 / Node ≥22 · 모노레포 Turborepo · 3앱 agent·api·app + 5패키지 · 에이전트 프레임워크 eve ^0.29.4 · 도구 19개·스킬 4개 · Prisma 7.9 / Postgres 17 · Better Auth(Google 전용) · 최신 커밋 2026-08-02 · TrendShift Daily #3)
trycompai/crm은 "영업 담당자가 연락처·회사·거래 정보를 손으로 채우는 대신, 스스로 도는 리서치 에이전트가 이메일 스레드·회의·서명란·웹을 뒤져 '이 사람이 누구인지'를 알아내고 증거와 함께 기록하는, 에이전트 우선(agentic-first)의 오픈소스 CRM"이다. 핵심 뒤집기 한 줄로 요약하면 — 에이전트는 CRM의 기능이 아니라, CRM이 에이전트의 메모장이다.
보통 CRM은 접수대의 빈 서류철이다. 담당자가 명함을 받아 이름·회사·직함을 칸칸이 손으로 옮겨 적어야 한다. 안 적으면 비어 있고, 잘못 적으면 잘못인 채로 남는다.
이 프로젝트는 접수대가 아니라 책상 하나를 통째로 차지한 리서치 조수다. 새 연락처가 들어오면 조수가 알아서 "이 사람 누구지?"를 조사한다 — 우리가 이미 가진 메일 스레드를 읽고, 서명란을 보고, 필요하면 LinkedIn·웹까지 확인한다. 알아낸 것은 "내가 이렇게 봤다"는 증거와 함께 기록하고, 확실치 않으면 담당자에게 "이 둘 중 누구입니까?"라고 되묻는다. 당신이 자리를 비워도(브라우저를 닫아도) 조수는 자기 할 일 목록을 보며 계속 일한다.
연락처 하나가 새로 생겼을 때 시스템 안에서 벌어지는 일을 압축하면 이렇다. 왼쪽(전통 CRM)과 오른쪽(이 레포)의 대비가 핵심이다:
오른쪽 흐름의 각 마디 — 큐, lease, 조사 세션, 증거 원장 — 가 이 레포에서 배울 만한 설계의 전부다. 4장에서 하나씩 뜯어본다.
이 레포가 뜬 이유는 기능이 많아서가 아니다. 별(star)은 이 글을 쓰는 시점에 아직 두 자릿수인 신생 저장소다(TrendShift의 굵은 숫자는 별 수가 아니라 모멘텀 점수다). 뜨는 이유는 "AI를 CRM에 어떻게 붙일 것인가"에 대한 지금까지의 답을 정면으로 반박하기 때문이다.
| 구도 | 지능이 사는 곳 | 일하는 방식 | 빈칸을 대하는 태도 |
|---|---|---|---|
| 전통 CRM | 없음 (사람이 지능) | 사람이 폼 입력 | 사람이 채울 때까지 빈칸 |
| AI 챗봇 CRM | 폼 옆의 챗박스 | 물어보면 답함 (요청-응답) | 여전히 사람이 채움 |
| trycompai/crm | 독립 배포된 에이전트 | 스스로 큐를 돌며 조사 | 에이전트가 증거로 채우거나, 못 채우면 제안 |
README의 표현을 빌리면: "대부분의 CRM은 앞에 폼을 세운 데이터베이스다. AI CRM은 그 폼 옆에 챗박스를 볼트로 조여 붙인다. 둘 다 진짜 일 — 무엇이 사실인지 알아내고 그것을 적어 두는 일 — 은 더 나은 할 일이 있는 사람에게 떠넘긴다." 이 레포는 그 "진짜 일"을 제품의 중심에 놓았다.
백엔드(NestJS API)는 일부러 아무 판단도 하지 않는다. 스레드가 하나 수집됐다, 회사가 생성됐다, 참석자가 누군지 모른다 — 이런 "무슨 일이 일어났다"를 큐에 한 행 적을 뿐이다. 그 행이 무슨 의미인지는 에이전트가 lease해서 결정한다. Nest 서비스가 직접 enrichment API를 호출하면 그것은 버그로 취급된다. 왜 이런 규칙이 생겼는지는 docs/api.md에 "그 규칙을 만들게 한 장애(outage)" 일화로 남아 있다.
이 레포에서 가장 배울 만한 설계다. 어떤 도구도 confidence: 0.9 같은 값을 받지 않는다. 이유가 날카롭다:
LLM에게 "이 사실을 얼마나 확신하니?"라고 물으면 모델은 점수를 만들어낸다 — 그리고 자기가 쓸모 있어 보이는 쪽으로 틀린다. 그래서 도구는 확신도 대신 관찰한 종류만 보고한다: crm.signature-block(상대 서명란이 그렇게 적혀 있다), github.account-identity(깃허브 계정 이름이 일치한다) 같은 것.
그 증거의 무게는 코드로 고정된 원장이 매긴다. "고객에 대해 자신 있게 틀린 사실은 빈칸보다 나쁘다 — 아무도 그게 틀렸는지 알 수 없으니까."
에이전트에게 셸(bash·grep·glob)과 /workspace를 주되, 샌드박스는 deny-all 이그레스(egress) — 바깥으로 나가는 네트워크가 완전히 차단된다. 게다가 샌드박스에는 DATABASE_URL을 절대 넘기지 않는다. 크레덴셜과 송신 경로를 둘 다 가진 셸은 "유출 모양(exfiltration-shaped)"이지만, 둘 다 없는 셸은 그냥 텍스트 처리기다. 고객 이메일 본문이 셸 명령으로 빠져나갈 유일한 경로를 구조적으로 없앤 것이다.
이 CRM은 의도적으로 단일 테넌트(single-tenant)·내부 전용이다. 로그인은 Google 하나, 접근 허용 목록은 환경변수 딱 하나(ALLOWED_SIGN_IN), 들어온 사람은 전부 다 본다. "항상 같은 값인 organizationId는 컬럼 하나, 인덱스 하나, 권한 체크 하나를 늘리면서 아무것도 사주지 못한다"는 이유로 조직(organization) 개념 자체를 뺐다. 스타트업 하나가 자기 영업에 쓰려고 만든 물건이라는 정직함이, 역설적으로 설계를 날카롭게 만든다.
한마디로 "Vercel 생태계로 끝까지 밀어붙인 TypeScript 모노레포"다. 런타임은 Node가 아니라 Bun, 빌드 오케스트레이션은 Turborepo, 세 개의 앱과 다섯 개의 공유 패키지가 하나의 저장소에 산다.
turbo run build). Bun = Node의 대체 런타임 겸 패키지 매니저 — 설치·실행이 빠르다. 이 레포는 package.json의 packageManager로 Bun 1.3.12를 못 박아 둔다.| 층 | 무엇 | 버전 / 비고 |
|---|---|---|
| 에이전트 | eve — 지속형 세션·도구·스킬·스케줄·샌드박스 | ^0.29.4 · Vercel의 파일시스템-우선 프레임워크 |
| 모델 | Vercel AI Gateway (@ai-sdk/gateway) | 공급자 SDK 없음 · Vercel OIDC라 관리할 키가 없음 |
| 샌드박스 | Vercel Sandbox(운영) / Docker · microsandbox(로컬) | 세 백엔드 모두 deny-all 이그레스 |
| 프론트엔드 | Next.js App Router · shadcn/ui · nuqs | Next 16.2.12 · nuqs 2.8 (URL 상태) |
| API | NestJS + nestjs-trpc | Nest 11 · trpc 브리지 2.13 · HTTP·인증·Google 동기화 |
| 데이터 | Prisma · Postgres(Neon) · 선택 Redis(Upstash) | Prisma 7.9 · Postgres 17-alpine(로컬 docker) |
| 인증 | Better Auth, Google 전용, 허용 목록 하나 | 1.6 · ALLOWED_SIGN_IN 하나가 인가 모델 전부 |
| 파일 | Vercel Blob — 프로필/로고 미러링 | 원본 URL이 만료돼도 사진이 남도록 복제 저장 |
| 툴링 | Biome · TypeScript 5.9 · Turborepo · Bun | Node ≥22 · 포맷/린트는 Biome 하나로 통일 |
AppRouter 타입을 생성해, Prisma의 DB 행부터 화면의 표 셀까지 타입이 이어진다. (생성물 apps/api/src/generated/server.ts는 커밋되며 빌드 때 재생성 금지 — 생성기가 요구하는 GLIBC가 빌드 이미지보다 높기 때문.)눈에 띄는 선택은 Vercel AI Gateway다. OpenAI/Anthropic SDK를 직접 붙이지 않고 게이트웨이를 거치면, Vercel 위에서는 OIDC로 인증되어 API 키를 코드나 환경변수에 둘 필요가 없다. 모델 선택은 DB 설정(AppSetting)에서 읽어 세션 시작 때 동적으로 바뀐다(lib/model.ts의 selectedModel()). 무거운 GPU도, 모델 프로바이더 계정도 이 앱이 직접 들고 있지 않다.
앱(Next.js)·API(NestJS)·에이전트(eve)는 서로 독립된 세 배포판이고, 이들이 반드시 합의해야 하는 건 DATABASE_URL과 BETTER_AUTH_SECRET 둘뿐이다. 모든 소통은 Postgres를 통해 간접적으로 일어난다.
FOR UPDATE SKIP LOCKED)에이전트는 크론 표현식으로 "매 N분마다 오래된 연락처 10명" 같은 걸 하지 않는다. 그런 건 전부 각 작업의 dueAt(마감 시각)에 들어간다. 스케줄러(dispatch.ts)는 매분 깨어나 지금 마감된 일만 lease한다.
FOR UPDATE SKIP LOCKED · lease(리스)leasedUntil). 일꾼이 죽어 임대가 만료되면 그 행은 자동으로 풀려 다른 일꾼이 다시 집는다. 크론·메시지큐 없이 DB 하나로 신뢰성 있는 작업 분배를 얻는다.// lib/tasks.ts — 마감된 일을 원자적으로 lease (핵심만)
export async function claimDue(limit) {
return db.$queryRaw<LeasedTask[]>`
UPDATE "agentTask" AS t
SET "leasedUntil" = ${until}, -- 10분 임대
"attempts" = t."attempts" + 1
FROM (
SELECT id FROM "agentTask"
WHERE "finishedAt" IS NULL
AND "dueAt" <= ${now} -- 지금 마감된 것만
AND ("leasedUntil" IS NULL OR "leasedUntil" < ${now})
AND "attempts" < 3 -- 3번 실패하면 포기
ORDER BY "priority" DESC, "dueAt" ASC
LIMIT ${limit}
FOR UPDATE SKIP LOCKED -- 겹치지 않게 나눠 집기
) AS due
WHERE t.id = due.id
RETURNING t.id, t."contactId", t.kind, t.reason, t.budget;`;
}
에이전트가 나중에 이 사람을 또 보고 싶으면 schedule_recheck를 호출하며 이유를 적는다 — 그리고 그 이유가 담당자에게 그대로 보인다. "14일 뒤에 다시 볼게요"라고만 하고 이유를 못 대는 에이전트는 이유가 있는 게 아니라 그냥 기본값을 쓰는 것이기 때문이다.
evidence.ts)2장에서 예고한 그 원장이다. 증거는 종류(kind)별로 무게가 코드에 박혀 있고, 여러 증거는 확률적으로 결합된다. 핵심은 "독립된 소스 하나당 증거 한 개" — 같은 페이지에서 이름과 회사가 둘 다 맞는 건 증거 두 개가 아니라 한 개다(안 그러면 한 페이지를 이중으로 세어 거짓 확신이 된다).
// evidence.ts — 증거 무게(일부)와 결합
WEIGHTS = {
"profile.email-match": { weight: 0.95, primary: true }, // 프로필에 그 이메일
"linkedin.employer-and-name": { weight: 0.85, primary: true }, // 이름+회사 동시 일치
"crm.signature-block": { weight: 0.80, primary: true }, // 본인 서명란
"github.account-identity": { weight: 0.80, primary: true },
"web.cited-claim": { weight: 0.40, primary: false }, // 보조: 혼자선 부족
"employer-only": { weight: 0.20, primary: false }, // 회사만 맞음(거의 무의미)
"contradiction": { weight: 0.00 }, // 모순: 통째로 hold
};
// 결합: 1 - ∏(1 - wᵢ) → 증거가 쌓일수록 1에 수렴, 상한 0.99
score = 1 - evidence.reduce((rem, e) => rem * (1 - WEIGHTS[e.kind].weight), 1);
// primary 증거 없이 높은 점수만으로는 VERIFIED가 안 됨
band = score >= 0.85 && hasPrimary ? VERIFIED
: score >= 0.55 ? PROBABLE
: score >= 0.30 ? POSSIBLE : null;
한 소스는 회사 A, 메일 헤더는 회사 B라고 말하면 그 사실은 "60% 참"이 아니라 미해결이다. contradiction 증거는 점수를 0.45로 눌러 밴드를 무너뜨린다 → 기록에 쓰지 않고 담당자에게 넘긴다. "제안(suggestion)은 실패가 아니라 종종 올바른 결과다." 같은 회사에 마르케티가 넷이면, 사람이 3초 만에 고르는 게 맞다.
sandbox.ts)단 8줄이지만 보안 사고방식이 압축돼 있다. 세 실행 백엔드(Vercel/Docker/microsandbox) 전부에 나가는 네트워크 전면 차단을 건다.
// sandbox/sandbox.ts — 전부
export default defineSandbox({
backend: defaultBackend({
vercel: { networkPolicy: "deny-all" },
docker: { networkPolicy: "deny-all" },
microsandbox: { networkPolicy: "deny-all" },
}),
});
차단이 공짜인 이유가 영리하다. web_fetch는 앱 런타임에서, web_search는 모델 공급자 쪽에서 돈다 — 즉 바깥과 통신하는 도구들은 애초에 샌드박스 밖에 있다. 그래서 샌드박스에서 네트워크를 없애도 기능은 그대로고, 사라지는 건 딱 하나 — 고객 이메일 본문이 셸 명령으로 빠져나갈 경로다. 여기에 "샌드박스에는 DATABASE_URL을 절대 주지 않는다"가 더해지면, 셸은 크레덴셜도 송신 경로도 없는 순수 텍스트 처리기가 된다.
NestJS API의 agent/ 모듈은 agent-queue.service와 agent-trigger.service뿐이다. 하는 일은 "연락처가 생겼다 → agentTask 한 행 넣기". 판단(누구인지, 무엇을 조사할지)은 전부 에이전트 몫이다. 이 경계가 흐려지면 무슨 일이 생기는지 docs/api.md가 증언한다: "신원 매처가 두 벌로 복제됐다가 서로 갈라져, 결국 한쪽이 지구상 모든 고용주를 일치로 판정했다." 지능을 한 곳(에이전트)에만 두는 이유다.
.agents/skills/ 아래에는 이 레포를 만드는 데 쓰인 코딩 에이전트용 스킬팩이 통째로 들어 있다 — eve, shadcn, nestjs-trpc, nuqs, turborepo, prisma-database-setup, vercel-react-best-practices, nestjs-best-practices 등. 즉 "에이전트 우선 CRM"은 코드 자체도 에이전트와 함께 지어졌다. 다만 README는 못을 박는다 — "에이전트가 쓴 PR보다, 당신이 쓴 한 문단을 더 원한다."
| 도구 | 역할 |
|---|---|
| read_crm_history | 우리가 이미 가진 스레드·회의·서명란을 읽는다(무료, 최고의 증거) |
| identify_contact | 이 연락처가 실제로 누구인지 특정 |
| research_person / research_company | 웹·LinkedIn으로 배경 조사 (키가 있을 때만) |
| record_fact | 주장 1개 + 증거를 원장에 기록 → 확정 or 제안 결정 |
| schedule_recheck | 이유를 적고 다음 확인을 큐에 예약 |
| write_brief | 담당자가 회의 전에 읽을 요약(brief) 작성 |
| list_outstanding_work | 지금 밀려 있는 조사 대기열 확인 |
이 저장소는 "CRM 만드는 법"보다 "에이전트를 신뢰할 수 있게 만드는 법"의 교재에 가깝다. 층별로 배울 것을 정리한다.
도구는 파일, 스킬은 마크다운 파일, 스케줄도 파일, 샌드박스도 파일. 에이전트가 "디렉토리 하나"라는 발상. 세션이 재배포를 견디고 하던 일을 이어 하는 durable 실행이 핵심.
실습: tools/에서 defineTool + zod 스키마 한 개를 그대로 베껴 새 도구를 만들어 보고, 에이전트가 언제 그 도구를 부르는지 로그로 관찰.
FOR UPDATE SKIP LOCKED + lease(임대) + attempts 재시도 + 멱등 예약(scheduleTask가 미완료 동일 작업이 있으면 새로 안 만듦). RabbitMQ·SQS 없이 Postgres 하나로 작업 분배·장애 복구를 얻는 범용 백엔드 패턴.
실습: claimDue를 두 세션에서 동시에 호출해도 같은 행이 두 번 안 나오는지 확인. leasedUntil을 과거로 바꿔 "죽은 일꾼"의 일이 풀리는지 재현.
LLM에게 confidence를 물으면 안 되는 이유, 그리고 그 대안(관찰 종류 → 코드가 무게 부여 → 확률 결합 → 밴드). AI가 데이터를 쓰는 모든 제품이 참고할 만한 안티-환각(anti-hallucination) 설계.
실습: evidence.ts의 WEIGHTS에 새 kind를 추가하고 무게를 조정해, 같은 증거 조합이 VERIFIED↔PROBABLE로 넘어가는 경계를 직접 흔들어 보기.
deny-all 이그레스 + 크레덴셜 미주입 = 셸에 능력을 주되 유출 경로는 구조적으로 제거. "이 조합이면 무엇이 새어 나갈 수 있는가?"를 먼저 묻는 위협 모델링 사고.
실습: 샌드박스 안에서 curl이 실패하는 것을 확인하고, 왜 web_fetch(앱 런타임)는 되는데 셸 네트워크는 안 되는지 경로를 그려 보기.
NestJS 라우터에서 AppRouter 타입을 뽑아 프론트가 그대로 소비 → DB 행 타입이 표 셀까지 이어진다. 그리고 목록 상태(필터·정렬·페이지)는 URL에(nuqs) — "뷰가 곧 링크"라 주소창 복사로 화면이 재현된다.
실습: 연락처 목록에 필터를 걸고 주소창을 복사해 새 탭에 붙여넣어 같은 뷰가 뜨는지 확인. Prisma 모델에 필드 하나를 더하고 타입이 화면까지 전파되는 경로 추적.
인가 모델이 환경변수 하나(ALLOWED_SIGN_IN). 값이 비면 아무도 못 들어온다 — 안전한 방향으로 실패한다. Google Workspace면 동의 화면을 Internal로 두어 조직 밖은 프롬프트조차 못 본다. "권한을 늘려 복잡하게"의 반대 방향.
실습: ALLOWED_SIGN_IN을 도메인/개인메일/혼합으로 바꿔 가며 로그인 허용 경계가 어떻게 변하는지 확인.
docker compose up -d로 즉시(포트 5432, postgres 17-alpine).| 변수 | 무엇을 넣나 |
|---|---|
| BETTER_AUTH_SECRET | openssl rand -base64 32 결과 |
| ALLOWED_SIGN_IN | 허용 도메인/이메일 (예: acme.com 또는 you@gmail.com) — 인가 모델 전부 |
| GOOGLE_CLIENT_ID / _SECRET | Google OAuth 클라이언트(웹) — Gmail·Calendar API 활성화 필요 |
DATABASE_URL은 docker compose Postgres에 이미 맞춰져 있어 손댈 필요가 없다. Google 로그인이 유일한 문이므로, 이 값들이 없으면 API가 아예 뜨지 않는다(안전한 방향).
핵심 설계 철학: 외부 소스는 전부 선택이며, 하나도 없어도 작동한다. 에이전트는 세션 시작 때 "이 설치본에 무엇이 켜져 있나"를 통보받아, 없는 걸 뒤늦게 하나씩 실패로 발견하지 않고 미리 계획한다.
// 시작 로그 예시 — 켜진 능력과 꺼진 능력
[agent] on LinkedIn (RAPIDAPI_KEY)
[agent] off Web research (PERPLEXITY_API_KEY)
[agent] off Company brand data (CONTEXT_DEV_API_KEY)
| 키(선택) | 열어 주는 것 |
|---|---|
| RAPIDAPI_KEY | LinkedIn 프로필 — 이름·직함·재직 기간(신원에 권위 있음) |
| PERPLEXITY_API_KEY | 인용이 붙은 웹 리서치, LinkedIn slug를 찾는 검색 |
| CONTEXT_DEV_API_KEY | 도메인에서 회사 로고·업종·위치·소셜 |
| BLOB_READ_WRITE_TOKEN | 로고·프로필 사진 저장(원본 URL은 만료되므로 복제 보관) |
키가 하나도 없어도 read_crm_history가 우리 자신의 스레드·회의·서명란을 읽는다 — 공짜이고, "상대 본인 주소에서 온 답장"만큼 좋은 증거는 어떤 데이터 업체도 팔 수 없다.
세 배포판(app·api·agent) + Postgres 하나. 셋이 반드시 합의해야 하는 건 DATABASE_URL과 BETTER_AUTH_SECRET뿐 — 어긋나면 에러가 아니라 리다이렉트 루프로 드러난다. Gmail/Calendar 동기화를 유지하려면 CRON_SECRET을 세우고 스케줄러가 POST /internal/sync/google를 치게 한다.
git clone → bun install → docker compose up -d → .env에 4개 값 → bun run db:deploy → bun run db:seed → bun run dev. localhost:3000에서 그럴듯한 파이프라인을 눈으로 확인하고, 각 연락처의 Agent 탭에서 에이전트가 남긴 단계 로그를 읽어 본다.
lib/capabilities.ts의 배열에 새 항목(예: 가짜 MYSOURCE_API_KEY)을 추가하고, 시작 로그의 on/off 목록과 capabilitiesMarkdown() 출력이 어떻게 바뀌는지 관찰. "없는 소스를 미리 알려 주는" 설계가 코드로 어떻게 구현됐는지 체감.
tools/에서 read_crm_history.ts를 템플릿 삼아, 연락처의 최근 활동 N건을 반환하는 새 defineTool을 작성(zod로 입력 스키마 정의). description을 바꿔 가며 에이전트가 그 도구를 부르는 빈도가 어떻게 달라지는지 본다 — 도구 설명이 곧 프롬프트라는 걸 체감.
evidence.ts에 새 EvidenceKind를 넣고 무게를 정한 뒤, test/evidence.spec.ts에 케이스를 추가해 특정 증거 조합이 어느 밴드로 떨어지는지 검증. contradiction을 하나 섞으면 점수가 어떻게 무너지는지 스냅샷으로 남긴다.
schedules/dispatch.ts의 work(kind, reason) switch에 새 kind(예: "birthday-check")를 추가하고, scheduleTask로 dueAt을 미래로 설정해 큐에 넣는다. 매분 도는 dispatch가 마감 시각에 그 일을 집어 세션을 여는 전체 경로를 로그로 따라가 본다.
| 주차 | 주제 | 구체적으로 |
|---|---|---|
| 1주 | 모노레포 & 타입 파이프라인 | Turborepo·Bun 워크스페이스 이해 → nestjs-trpc로 서버 타입이 프론트까지 흐르는 경로 추적. nuqs로 "URL이 상태"를 구현. |
| 2주 | Postgres 동시성 | FOR UPDATE SKIP LOCKED·트랜잭션·advisory lock. lease/재시도/멱등 예약으로 "정확히 한 번" 비슷한 보장을 DB로 만드는 법. |
| 3주 | 지속형 에이전트(eve) | node_modules/eve/docs/부터 정독. 도구/스킬/스케줄/채널/샌드박스를 파일로 정의하고 durable 세션을 설계. |
| 4주 | 증거·확신 모델링 & 평가 | 확신도를 안 받는 원장 설계, 밴드 임계값 튜닝, eval로 회귀 방지. "제안이 좋은 결과"라는 UX까지. |
| 5주 | 에이전트 보안 | deny-all 이그레스, 크레덴셜 격리, 위협 모델링("무엇이 새어 나갈 수 있나"). 최소권한 셸과 감사 로그(hooks/audit). |
다섯 주가 결국 한 문장으로 모인다 — "어떻게 하면 자율적으로 도는 에이전트를 믿을 수 있게 만드는가." 큐(2주)는 안 죽게, 원장(4주)은 거짓말 안 하게, 샌드박스(5주)는 못 새어 나가게 만든다. 기능이 아니라 신뢰성이 이 저장소의 진짜 교재다.
dueAt(마감)·leasedUntil(임대)·attempts(시도)로 관리되며, dispatch가 매분 마감된 것만 집는다.evidence.ts의 구조. 환각을 막는 안티패턴의 반대편.DATABASE_URL 미주입과 합쳐져, 셸을 "유출 불가능한 모양"으로 만든다.@ai-sdk/gateway).server.ts는 커밋되고 빌드 때 재생성 금지.README.md(철학·스택) · docs/agent.md(에이전트 전체 서술) · docs/api.md("API에 지능 없음" 규칙) · docs/design.md · adrs/comp-palette.mdapps/agent/agent/lib/tasks.ts(일 큐) · lib/evidence.ts(증거 원장) · schedules/dispatch.ts(디스패처) · sandbox/sandbox.ts(deny-all)node_modules/eve/docs/에 동봉