senpi는 무엇을 보고 판단하는가
senpi는 pi-mono를 포크해 "자주 쓰는 건 그냥 바이너리에 넣자"고 베팅한 코딩 에이전트입니다. 설치본을 열어보니 판단 규칙이 전부 읽을 수 있는 자리에 있었습니다. 여기서는 시스템 프롬프트가 어떤 순서로 조립되는지, 그 안에서 의도·탐색·검증·중단이 각각 어떻게 강제되는지를 실제 코드로 따라갑니다.
1. 한 장 요약
senpi의 판단 장치는 매 턴 새로 조립되는 시스템 프롬프트 한 장입니다. 상류 pi-mono의 정적 프롬프트를 걷어내고, 블록을 함수로 쪼개 순서대로 쌓습니다. 이 순서 자체가 설계 의도입니다 — 먼저 의도를 못 박고, 병렬로 긁어모으고, 언제 멈출지 정하고, 그다음에야 검증 기준을 준다.
"신중한 시니어 엔지니어의 작업물과 구별되지 않아야 한다" 한 문장. 그게 전부입니다.
매 메시지마다 "나는 이걸 이렇게 읽었고, 계획은 이렇다" 한 줄을 강제. 표면형이 아니라 진짜 의도로 라우팅.
독립적인 호출은 한 웨이브로 몰아친다. 낭비 읽기는 싸고, 낡은 가정은 턴 전체를 태운다.
기억을 믿지 말고 다시 읽어라. 그리고 언제 그만 찾을지를 조건 세 개로 못 박는다.
변경 크기에 따라 V1·V2·V3. 범위는 티어링하되 엄격함은 티어링하지 않는다.
이번 턴에 실제로 켜진 도구만 분류해서 붙인다. 없는 도구는 언급조차 금지.
하드 블록 3개 + 안티패턴 3개. 짧고 단호합니다.
말투 규칙이자 실행 스탠스. "가장 작은 올바른 변경이 이긴다."
모델별 튜닝 프리셋 → 프로젝트 지침 파일 → 스킬 → 실행 환경 → 오늘 날짜와 작업 디렉터리.
가재코드가 워크플로 네 단계로 판단을 강제했다면, senpi는 같은 일을 프롬프트 블록과 런타임 훅으로 합니다. 무엇을 강제하려는지는 놀랄 만큼 겹치고, 강제하는 방법이 정반대입니다. 그 비교가 이 글의 마지막 절입니다.
2. 판단 규칙이 놓인 자리
npm 전역 설치본에 컴파일된 JS가 소스맵과 함께 그대로 들어 있습니다. 난독화가 없어서 프롬프트 문자열을 원문으로 읽을 수 있습니다. 이 글의 인용은 전부 그 파일들에서 가져왔습니다.
| 무엇 | 설치본 경로 (dist/ 기준) |
|---|---|
| 프롬프트 조립 순서 | core/dynamic-prompt/build.js |
| 의도 게이트 | core/dynamic-prompt/intent-gate.js |
| 탐색 중단 조건 | core/dynamic-prompt/exploration.js |
| 검증 티어 + 테스트 규율 | core/dynamic-prompt/verification.js |
| 정책(하드 블록·안티패턴) | core/dynamic-prompt/policies.js |
| 스타일·실행 스탠스 | core/dynamic-prompt/style.js |
| 권한 프리셋 정의 | core/extensions/builtin/permission-system/config.js |
| 컨텍스트 압축 상수 | core/extensions/builtin/compaction/{policy,circuit-breaker,per-turn-cap}.js |
| 요약 아카이비스트 프롬프트 | core/extensions/builtin/compaction/prompts.js |
| todo 도구 계약 | core/extensions/builtin/todotools/prompt.js |
| bash 타임아웃 정책 | core/extensions/builtin/bash-timeout/timeout.js |
구조 차이 한 줄. 가재코드는 판단 규칙을 마크다운 계약서로 두고 런타임이 읽어옵니다. senpi는 TypeScript 함수가 문자열을 반환하는 형태라 조건부 조립(켜진 도구에 따라 문장이 바뀌는 것)이 자연스럽습니다. 대신 사용자가 규칙만 갈아끼우기는 어렵습니다 — 확장으로 덧붙이거나 프리셋을 쓰는 쪽으로 설계돼 있습니다.
3. 턴이 열릴 때: 의도 게이트
senpi에서 가장 특징적인 장치입니다. 모든 메시지는 라우팅 한 줄로 시작해야 합니다.
"매 턴을 사용자가 원하는 것과 당신의 계획을 밝히는 짧은 라우팅 한 줄로 열어라: I read this as [intent] - [plan].
이 줄은 필수다. 당신의 독해를 투명하게 만들 뿐, 구현을 약속하지는 않는다 — 구현을 약속하는 건 사용자의 명시적 요청뿐이다."
핵심은 뒤 문장입니다. 읽었다고 해서 만들기로 한 게 아니다. 에이전트가 "설명해줘"를 "고쳐줘"로 착각해 파일을 건드리는 사고를 표 하나로 막습니다.
| 사용자가 이렇게 말하면 | 진짜 의도 | 행동 |
|---|---|---|
| "X 설명해줘", "Y 어떻게 동작해?" | 리서치 | 코드를 읽고 답한다. 편집 없음. |
| "X 구현해줘", "Y 추가해줘" | 구현 | 먼저 진단하고, 만든다. |
| "X 좀 봐줘", "Y 확인해줘" | 조사 | 찾아 읽고 보고. 아직 고치지 않는다. |
| "X 어떻게 생각해?" | 평가 | 판단하고 제안한 뒤 확인을 기다린다. |
| "X 에러가 나", "Y가 깨졌어" | 수정 요청 | 에러에서 진단해 최소로 고친다. |
| "리팩터링", "개선", "정리" | 열린 변경 | 먼저 진단하고 접근법을 제안한다. |
여기에 요청 종류별 범위 규칙이 붙습니다.
- 사소함 — 그냥 답한다.
- 명시적 — 시킨 것만 정확히. 추가 범위 금지.
- 탐색적 — 제안이나 변경 전에 관련 코드를 먼저 본다.
- 열린 요청 — 목표를 완전히 만족시키는 가장 짧은 경로를 택한다.
- 모호함 — 모호함을 이름 붙여 말하고, 가능한 한 맥락에서 스스로 해소한다.
그리고 게이트가 두 개 더 있습니다. 턴-로컬 의도 리셋은 매 턴 사용자의 마지막 발화를 백지에서 다시 읽고, 방향이 바뀌었으면 이전 계획을 버리라고 지시합니다 — 대기 중인 후속 지시와 조종 메시지가 이전 의도를 이깁니다. 컨텍스트 완결 게이트는 반대 방향의 브레이크입니다: 답이 코드·테스트·런타임 동작에 달렸으면 먼저 조사하되, 맥락이 충분해지면 그만 뒤지고 실행하라.
4. 근거를 모으는 방식
멈출 조건을 먼저 정한다
"파일 내용에 대한 기억은 믿을 수 없다 — 주장하거나 편집하기 전에 다시 읽어라. 한 웨이브가 핵심 질문에 답했거나, 같은 사실이 독립된 두 소스에서 나왔거나, 두 웨이브가 아무것도 더하지 못하면 탐색을 멈춰라. 다시 찾는 것은 종합 과정에서 새로운 미지가 드러났을 때뿐이고, '혹시 몰라서' 훑는 건 절대 아니다."
dist/core/dynamic-prompt/exploration.js — 전문에이전트 프롬프트 대부분은 "충분히 조사하라"만 말하고 언제 그만둘지를 안 말합니다. 그 결과가 같은 파일을 여섯 번 읽는 토큰 낭비죠. senpi는 중단 조건을 세 개로 못 박았고, 재탐색의 정당한 사유까지 한정했습니다. 짧지만 이 문단이 이 프로젝트에서 제일 밀도가 높습니다.
대신 한 번에 넓게 긁는다
"호출이 독립적이면 같은 응답에서 한 웨이브로 쏴라 — 읽기, 검색, 목록, 진단. 맥락이 얇을 때는 병렬 탐색으로 세게 편향하라: 조금이라도 관련 있어 보이면 나중에 순차로 말고 지금 다 끌어와라. 낭비된 읽기는 거의 공짜지만, 낡은 가정 위에서 움직이면 턴 전체를 날린다."
dist/core/dynamic-prompt/parallel-tools.js"값이 필요할 때만 순차로. 빠진 파라미터를 플레이스홀더로 채우지 마라"까지 붙어 있습니다. 모르는 인자를 그럴듯하게 지어내는 실패를 정면으로 겨냥한 문장입니다.
기억이 아니라 기록에서 꺼낸다
todo 도구 계약에도 같은 불신이 박혀 있습니다. 작업은 자동 생성 ID가 아니라 내용 문자열 그대로로 지목해야 하고,
정확한 텍스트를 잊었으면 view로 목록을 다시 받아 축어 복사해야 합니다 — 기억으로 재입력하는 건 금지입니다.
사용자가 다단계 계획을 주면 모든 항목을 각각의 작업으로 등록해야 하고,
"중요한 것만 골라" 요약하거나 나머지를 머릿속에 두는 건 명시적으로 막혀 있습니다.
덤: todo 호출만 단독으로 한 턴을 쓰는 것도 금지입니다("왕복 낭비"). 실제 작업과 같이 묶어 보내라고 못 박습니다.
5. 검증: 범위는 나누되 엄격함은 안 나눈다
"범위를 티어링하라. 엄격함은 절대 티어링하지 마라."
dist/core/dynamic-prompt/verification.js| 티어 | 대상 | 요구되는 증거 |
|---|---|---|
| V1 | 단일 파일, 동작 변화 없음 | 그 파일 진단. 끝. |
| V2 | 단일 도메인 동작 변경 | 변경 파일 진단(병렬) + 관련 테스트 + 영향받는 실행 진입점 1회 실행 |
| V3 | 다중 파일·횡단 변경 | 전 변경 파일 진단 + 관련 테스트 + 빌드 + 실제 표면으로 사용자 눈에 보이는 동작 수동 확인 |
그 아래에 테스트 규율 6조가 붙습니다. 전부 "테스트가 운으로 통과하는 것"을 막는 조항입니다.
- 테스트 코드를 읽거나 고칠 때 비결정성은 버그로 취급한다. 타이밍 운으로 통과해선 안 된다.
- 시간 자체가 검증 대상이 아닌 한 고정 sleep·폴링 지연·시간 대기 패턴 금지.
- 비동기는 액션을 트리거하기 전에 정확한 이벤트/상태 변화를 구독하고, 바운드된 타임아웃으로 그 신호를 기다린다.
- 목(mock)은 검증하려는 계약을 보존해야 한다. 대상 통합이 실패할 수조차 없을 만큼 격리하지 마라.
- 프롬프트 테스트는 문장을 그대로 고정하지 말고 동작·결정·구조·파싱된 규칙 데이터를 단언한다.
- 관련 테스트 명령을 한 번 돌리고 그 통과를 신뢰할 수 있게 만든다(단일 실행 통과).
"'통과할 것이다'는 검증이 아니다. 검증기를 돌리지 않고 깨끗한 출력을 보고하는 것은 위반이다. 당신의 변경이 유발한 문제만 고치고, 기존 실패는 따로 언급하라."
dist/core/dynamic-prompt/verification.js마지막 문장이 실무적으로 중요합니다. 기존 실패까지 떠안고 고치려다 범위가 폭발하는 걸 막고, 동시에 그걸 조용히 덮는 것도 막습니다.
6. 모를 때 · 넘칠 때
senpi의 불확실성 처리는 "묻기"가 아니라 금지 + 브레이크로 되어 있습니다.
- 모르면 읽는다, 알면 멈춘다
컨텍스트 완결 게이트: 답이 코드·테스트·런타임에 달렸으면 먼저 조사한다. 충분해지면 브라우징을 계속하지 말고 행동한다.
- 읽지 않은 것은 말하지 않는다
하드 블록: "읽거나 검증하지 않은 코드·테스트·런타임 동작에 대해 절대 추측하지 마라." 환각 보고를 정책 레벨에서 차단합니다.
- 커밋은 명시적 요청 없이는 절대
나머지 두 하드 블록도 같은 성격입니다: 타입 에러·린트 경고·테스트 실패를 우회하려고 억누르지 마라.
- 막혔을 때 하지 말아야 할 것 3종
실패하는 테스트를 지우거나 스킵해서 초록불 만들기, 의도 없이 에러 조용히 삼키기, 무관한 편집과 맹목적 재시도로 산탄총 디버깅 하기.
- 계획이 막히면 todo에 적는다
"Blocked? 활성 단계에 작업을 추가하거나 drop하라." 막혔다는 사실 자체를 상태로 남기게 합니다.
- 맥락이 넘치면 요약하되, 원문은 지키게 한다
압축 요약기는 "COMPACTION ARCHIVIST" 역할을 받고 규칙 네 개를 받습니다: 사용자 요청과 제약은 축어로 인용(의역 금지), 내용이 없는 섹션도 "None."으로 남기고 삭제 금지, 이전 요약의 요청·최종 목표·제약은 불변(덧붙이기만), 세션 ID·파일 경로·식별자는 바이트 단위로 보존. 산출 형식은 7개 섹션 고정(요청 축어 → 최종 목표 → 제약 → 완료한 일 → 활성 작업 맥락 → 남은 작업 → 정확한 다음 단계).
- 압축 자체가 폭주하지 않게 막는다
적응형 임계는 컨텍스트의 40~70% 사이에서만 움직이고(수확이 좋으면 늦추고 나쁘면 당김), 투기적 압축은 임계의 75% 지점에서 미리 돕니다. 실패가 3회 쌓이면 서킷 브레이커가 60초 열리고, 한 턴에 소프트 캡 3회·하드 캡 10회를 넘지 못합니다.
- 오래 걸리는 명령은 미리 잘라둔다
bash 기본 타임아웃 120초, 권장 최대 600초. 그보다 오래 필요한 작업은 tmux 같은 백그라운드로 돌리라고 프롬프트에 박아둡니다. "명령이 영원히 도는 걸 가정하지 마라."
정리하면 senpi는 사람에게 되묻는 장치가 거의 없습니다. 모호하면 이름 붙여 말하고 맥락에서 스스로 풀라고 하고, 파괴적인 단계에서만 멈춥니다. 가재코드가 인터뷰·승인 게이트로 사람을 붙잡아 두는 것과 정확히 반대 방향의 선택입니다.
"행동하고, 그다음 보고하라. 사용자에게 뭘 묻기 전에 파일을 읽고 검색하라. 비파괴적인 다음 단계가 명백히 옳으면 같은 턴에서 해라 — 맨 분석이나 계획이나 약속으로 끝내지 마라. 예고성 표현('다음으로 ~하겠습니다')과 허락 구걸('~할까요?')은 금지. 파괴적 행동은 권장안을 말하고 멈춘다."
dist/core/dynamic-prompt/style.js — Execution Stance7. 권한 기본값 — 여기 주의
senpi는 opencode식 권한 시스템을 빌트인으로 옮겨왔습니다. 규칙은 도구별·패턴별로 allow/ask/deny를
정하고, TUI에서 한 번 허용 / 항상 허용 / 거부 / 사유와 함께 거부를 고르게 합니다. 잘 만들어져 있습니다.
다만 코드를 열어보면 기본 프리셋이 full-access입니다 — 즉 { permission: "*", pattern: "*", action: "allow" }.
규칙 병합 순서는 기본 프리셋 → 글로벌 설정 → 프로젝트 설정 → CLI 플래그라서, 아무것도 설정하지 않으면
권한 시스템은 켜져 있어도 전부 통과시킵니다. 저장소가 컨테이너 경계를 제공하지 않는다고 밝힌 것과 같은 맥락입니다.
| 프리셋 | 내용 |
|---|---|
full-access (기본값) | 전부 allow |
workspace | 기본 ask, 단 read·list·grep·edit·bash는 allow / 작업 디렉터리 밖 접근은 ask |
read-only | 기본 ask, read·list·grep만 allow / edit·bash는 ask |
ask | 전부 ask |
# 이번 실행만
senpi --permission-preset workspace
# 프로젝트에 고정: .senpi/settings.json
{ "permission": { "bash": "ask", "edit": "ask" } }
평가: 기본값을 열어두는 선택 자체는 이 도구의 성격(자기 작업 머신에서 빠르게 굴리는 러너)과 일관됩니다. 문제는 권한 시스템이 있다는 사실이 기본이 안전하다로 읽히기 쉽다는 점입니다. 남의 코드·회사 저장소에 붙일 거라면 프리셋을 먼저 바꾸고 시작하세요.
8. 가재코드와 비교 — 같은 병, 다른 처방
앞선 해부와 나란히 두면 재미있습니다. 두 프로젝트가 겨냥하는 실패는 똑같습니다: 에이전트가 확인하지 않은 것을 확인한 것처럼 보고한다. 처방이 정반대입니다.
| 축 | 가재코드 (gjc) | senpi |
|---|---|---|
| 강제 수단 | 워크플로 4단계 + 상태 파일 + CLI 게이트 | 시스템 프롬프트 블록 + 런타임 훅 |
| 규칙이 사는 곳 | 마크다운 계약서(교체·감사 쉬움) | TS 함수(조건부 조립 쉬움) |
| 모호할 때 | 인터뷰로 사람에게 묻는다(모호도 수치화) | 이름 붙여 말하고 맥락에서 스스로 푼다 |
| 실행 승인 | 사람 승인 없이는 제품 코드 못 건드림 | 파괴적 단계에서만 정지, 나머지는 그냥 함 |
| 검증 강제 | 품질 게이트 JSON을 CLI가 거부(표면별 증거 요구) | V1·V2·V3 티어를 프롬프트로 요구 |
| 증거 보존 | ledger.jsonl append-only 리시트 | 압축 요약의 축어 보존 규칙 |
| 탐색 규율 | 도구 우선순위·앵커로 정확도 확보 | 병렬 웨이브 + 중단 조건 3개 |
| 무게 | 무겁다(서브에이전트 다수, 게이트 다수) | 가볍다(단일 CLI, 프롬프트 한 장) |
어느 쪽이 낫냐는 일의 되돌리기 비용에 달렸습니다. 되돌리기 쉬운 내 저장소의 일상 작업이면 senpi 쪽이 훨씬 빠르고 덜 성가십니다. 되돌리기 비싼 일(마이그레이션, 인증, 공개 API 변경)에는 가재코드처럼 승인과 증거를 파일로 남기는 구조가 값을 합니다.
비판
- 프롬프트는 계약이 아니라 부탁입니다. "검증 없이 깨끗하다고 보고하는 건 위반"이라고 써 있어도, 위반을 막는 코드는 없습니다. 가재코드는 게이트 JSON을 CLI가 거부하죠. senpi에서 이 규율이 지켜지는지는 모델 성실성에 달렸습니다.
- 기본값이 전부 허용입니다. 권한 시스템이 있다는 사실과 안전한 기본값은 별개입니다.
- 포크 유지 비용. 상류 pi-mono를 주기적으로 리베이스하는 구조라,
changes.md규율이 있어도 상류가 크게 움직이면 흔들릴 여지가 있습니다. - 실험 단계. 저장소가 스스로 "프로덕션 파이프라인을 걸지 말라"고 못 박았고, 설치 시
~/.pi/agent를~/.senpi/agent로 통째 이관합니다(상류 pi 사용자는 백업 먼저 — 리뷰에 실측 기록).
그럼에도 배울 게 분명합니다. 특히 탐색 중단 조건과 검증 티어링은 다른 에이전트를 쓰든 자기 프롬프트에 그대로 옮겨 적을 만한 두 문단입니다.
9. 직접 확인해보기
설치돼 있다면 이 글의 인용은 전부 자기 디스크에서 확인됩니다.
npm install -g @code-yeongyu/senpi
senpi --version
# 설치 경로
cd "$(npm root -g)/@code-yeongyu/senpi/dist/core"
cat dynamic-prompt/build.js # 프롬프트가 어떤 순서로 조립되는지
cat dynamic-prompt/intent-gate.js # 의도 게이트 전문
cat dynamic-prompt/exploration.js # 탐색 중단 조건 (제일 짧고 제일 밀도 높음)
cat dynamic-prompt/verification.js # V1·V2·V3 + 테스트 규율 6조
cat dynamic-prompt/policies.js # 하드 블록 3 + 안티패턴 3
cat dynamic-prompt/style.js # 실행 스탠스
# 권한 기본값 확인 (여기 주의)
grep -n "DEFAULT_PERMISSION_PRESET" -A 20 extensions/builtin/permission-system/config.js
# 압축 상수
head -8 extensions/builtin/compaction/policy.js
cat extensions/builtin/compaction/circuit-breaker.js | head -3
공부 순서 추천. ① exploration.js(4줄) → ② verification.js의 티어 3줄 →
③ intent-gate.js의 의도 표. 이 셋이면 "에이전트에게 언제 멈추고 언제 증거를 요구할지"의 뼈대가 잡힙니다.
그다음 가재코드 해부와 나란히 놓고 같은 문제를 어떻게 다르게 푸는지 비교해보세요.
dist/의 컴파일 산출물과 저장소 README를 직접 읽고 정리한 독립 분석입니다.
senpi는 cmore.dev와 무관한 외부 오픈소스이고, 이 페이지는 보증이나 공식 문서가 아닙니다.
저장소가 스스로 실험 단계라고 밝힌 프로젝트라 버전마다 상수와 문구가 바뀔 수 있으니,
중요한 판단은 위 명령으로 본인 환경의 실제 파일을 확인하고 하세요.