report claude-code-main draft 2026-06-07

Pantheon 페르소나 도구 구조 디버깅 플레이북

reports/architecture/persona-tool-structure.html

TL;DR

관련 보고서: Pantheon 페르소나 도구·세션 아키텍처 (전략 수립용, 2026-06-08) — 같은 구조를 전략·옵션 A/B/C 관점으로 다룸. 본 문서와 결이 다르고 보완 관계.

Context

사고 발단

2026-06-07 #northstar 콜로세움 트래커 작업 중, 지니 페르소나가 Notion DB row를 가져오려다 MCP 도구 부재로 실패. 지니의 deferred tool 목록에는 mcp__notion__authenticate만 있고 notion-search/notion-fetch/notion-create-pages부재. 같은 시점 메인 Claude Code 세션에는 Notion MCP 도구 전체가 정상 노출 — 비대칭이 같은 머신·같은 사용자에서 발생했다.

지니가 임시로 메인 세션에 row fetch를 위임해 사건은 봉합됐지만, "지니가 어떤 구조로 도구를 쓰고 있는지 알아야 앞으로 도구 전략을 세울 수 있겠다"는 사용자 요청이 본 보고서의 출처다.

표기 규약

마커 의미
확정 사실 소스 인용(file:line 또는 config 발췌)으로 검증된 것
관측 콜로세움 케이스 등 실제 관찰됐지만 원인까지는 미확정인 것
추정 관측·구조를 근거로 한 가설. 검증 필요

본 보고서가 다루지 않는

진단 — 5섹션

1. Slack 메시지 → 페르소나 라우팅 → 세션 생성/복원 흐름

확정 사실 (pantheon/bridge.py, pantheon/router.py, pantheon/providers/claude.py:62-68)

sequenceDiagram
  autonumber
  participant U as 사용자 (Slack)
  participant SL as Slack Cloud
  participant SH as Bolt App<br/>(persona별)
  participant BR as bridge.py<br/>handle_user_message
  participant RT as router.py<br/>(haiku)
  participant SES as persona_sessions<br/>+ sessions.json
  participant CLI as Claude CLI subprocess<br/>(spawn per call)
  participant MCP as MCP 서버군

  U->>SL: 메시지 (멘션·DM·스레드 답글)
  SL->>SH: WebSocket 이벤트 (페르소나별 Bolt App 각자 수신)
  SH->>BR: app_mention / message 이벤트
  Note over BR: _dup_guard (30분 TTL) +<br/>_in_flight dict로 N개 봇 중복 응답 차단

  alt 새 메시지 + 태그 없음
    BR->>RT: route(text, personas)
    RT->>CLI: claude -p (haiku, 30s timeout)
    CLI-->>RT: ["jini"] 또는 ["jarvis"] fallback
    RT-->>BR: 응답할 페르소나 결정
  else 멘션·스레드 owner·force prefix
    BR-->>BR: 라우팅 skip (직접 결정)
  end

  BR->>SES: get_or_create_session(persona, thread_ts)
  SES-->>BR: session_id (없으면 None)

  BR->>CLI: subprocess.Popen([<br/>  "claude", "-p", prompt,<br/>  "--output-format", "stream-json",<br/>  "--model", "sonnet",<br/>  "--permission-mode", "bypassPermissions",<br/>  "--resume", session_id?<br/>], cwd=~/Workspace)

  Note over CLI: 새 프로세스. 환경변수 상속.<br/>cwd 기반 .mcp.json 로드.<br/>user-level ~/.claude.json 로드.

  CLI->>MCP: tool 호출 (노출된 것만)
  MCP-->>CLI: 결과
  CLI-->>BR: stream-json (type=result, session_id)
  BR->>SES: save session_id (다음 회차 --resume용)
  BR->>SH: say(reply, thread_ts=...)
  SH->>SL: 페르소나 봇 토큰으로 게시
  SL->>U: 페르소나 명의 응답

핵심 사실 묶음:

항목
페르소나 Slack 진입점 페르소나별 독립 Bolt App + 독립 *_BOT_TOKEN + 독립 SocketModeHandler
중복 응답 차단 _dup_guard (30분 TTL 파일) + _in_flight dict — 같은 메시지가 N개 봇에 도달해도 1회만 응답
라우팅 (새 메시지·태그 없음) router.py Haiku 호출, fallback ["jarvis"]
세션 영속화 sessions.json {persona}:{thread_ts}claude --resume <session_id>
세션 단위 스레드가 영속 대화 단위. DM은 thread_ts=None — 채널별 상태로 별도 관리
LLM 프로세스 수명 매 메시지마다 새로 spawn. 응답 끝나면 종료. 다음 회차는 같은 session_idClaude 측 세션을 복원

핵심 함의: "지니가 기억한다"는 것은 Pantheon이 다음 호출에 session_id를 다시 넘긴다는 뜻이지, LLM 프로세스가 살아 있다는 뜻이 아니다. 따라서 프로세스 인스턴스 상태(OAuth 캐시 포함)는 매번 새로 만들어진다.

2. 페르소나별 MCP 도구 노출 방식

확정 사실 (~/.claude.json mcpServers, ~/Workspace/.mcp.json, ~/.claude/plugins/.../.mcp.json)

Claude CLI 서브프로세스가 시작될 때 MCP 도구가 노출되는 경로는 세 곳에서 합쳐진다.

flowchart TB
  subgraph SPAWN["페르소나 Claude CLI subprocess (spawn 시점)"]
    direction TB
    CWD["cwd = ~/Workspace<br/>(providers/claude.py:28)"]
  end

  subgraph CONFIGS["MCP 설정 합성"]
    direction TB
    C1["① ~/.claude.json<br/>mcpServers<br/>(user-level)"]
    C2["② &lt;cwd&gt;/.mcp.json<br/>(workspace-scoped)"]
    C3["③ ~/.claude/plugins/&lt;plugin&gt;/<br/>.mcp.json + 캐시<br/>(plugin manifest)"]
  end

  SPAWN -->|읽음| C1
  SPAWN -->|읽음| C2
  SPAWN -->|enabledPlugins 기준 활성화| C3

  C1 -->|"chrome-devtools, calendar, slack, slack-hangman,<br/>linear-davinci, linear-hangman"| TOOLS["deferred tools 목록"]
  C2 -->|gemini| TOOLS
  C3 -->|notion@claude-plugins-official,<br/>code-review@claude-plugins-official| TOOLS

  TOOLS -->|ToolSearch query=select:&lt;name&gt;| LOADED["로드된 도구<br/>(실제 호출 가능)"]

세 경로의 토큰/인증 보관 방식이 결정적으로 다르다.

경로 MCP 예시 인증 보관 위치 페르소나 subprocess 전파
① user-level config (Bearer/API key) slack-hangman (Bearer xoxp-…), linear-hangman/davinci (HTTP), calendar (env GOOGLE_OAUTH_CREDENTIALS=파일경로), chrome-devtools (env 없음) config 파일 본문에 명시 또는 외부 파일 경로 명시 ✅ 정상 — 파일을 매번 다시 읽으면 됨
② workspace .mcp.json gemini (env GEMINI_API_KEY) config 파일 본문에 명시 ✅ 정상
③ plugin MCP (OAuth, runtime) notion (https://mcp.notion.com/mcp, 토큰 명시 없음) 사용자 본 세션의 런타임 OAuth flow 결과 (캐시 위치 미확정 — .claude.json 상위 키 또는 별도 캐시 파일 추정) ⚠️ 세션 인스턴스에 따라 전파 불확실

ToolSearch / deferred tools 패턴: Claude CLI는 시작 시 모든 MCP에 connect하지 않고, 도구 메타데이터(이름)만 deferred 목록에 등재한 뒤, 실제 호출 직전에 ToolSearch query="select:<name>"로 스키마를 로드한다. 따라서 MCP 서버 연결은 lazy이고, 연결 시점의 인증 상태가 노출되는 도구 집합을 결정한다.

→ Notion plugin의 경우, 페르소나 subprocess가 시작될 때 mcp.notion.com에 인증 없이 핸드셰이크하면 서버가 authenticate tool만 반환하고 notion-search/notion-fetch권한 없는 상태로는 노출하지 않는다 (추정).

3. 메인 Claude Code 세션 ↔ Pantheon 페르소나 세션 분리 지점

확정 사실 (providers/claude.py:28, 62-68, ~/.claude.json, ~/.claude/settings.json)

같은 머신·같은 사용자(hangman) 하에서 도는 두 영역이지만, 어디까지가 공유고 어디부터가 별개인지가 본 보고서의 핵심.

항목 메인 Claude Code 세션 Pantheon 페르소나 세션 공유?
프로세스 사용자 터미널에서 띄운 claude CLI (장기 실행) bridge.py가 매 메시지마다 subprocess.Popen(["claude", "-p", ...])
cwd 사용자가 띄운 위치 (보통 ~/Workspace) Path(__file__).resolve().parents[2] = ~/Workspace (providers/claude.py:28) ✅ 보통 같음
~/.claude.json (user-level) 읽음 읽음 ✅ 파일 자체는 공유
~/.claude/settings.json (enabledPlugins) 읽음 읽음 ✅ 파일 자체는 공유
MCP 토큰 (Bearer/API key) 위 파일 본문에서 그대로 사용 위 파일 본문에서 그대로 사용
MCP 토큰 (OAuth, runtime) 메인 세션이 최초 1회 OAuth flow를 거쳐 캐시 보유 페르소나 subprocess는 그 캐시를 인스턴스 상태로 들고 있지 않음 (추정) ⚠️ 불확실
Claude 측 conversation session 사용자가 인터랙티브로 누적 --resume <session_id>로 Claude 측 세션 복원 — 서로 다른 session_id pool
환경변수 사용자 셸 환경 bridge.py가 띄울 때의 환경 (Pantheon .env + 시스템 환경) 부분적
인터럽트/재시작 영향 사용자가 직접 통제 bridge.py 재시작 시 모든 진행 중 LLM 호출 종료

핵심 분리 지점 = OAuth 캐시: Bearer/API key는 config 파일 본문에 있거나 없거나이므로 이진 결정이고 페르소나도 동일하게 작동한다. 반면 OAuth는 세션 인스턴스의 메모리/캐시 상태에 의존하는 경우가 있어 페르소나 subprocess가 fresh state로 시작할 때 권한 누락 가능성이 있다.

4. Notion 인증만 실패한 원인 후보 (대표 케이스)

콜로세움 케이스의 관측:

MCP 페르소나(지니) 측 도구 메인 세션 측 도구 인증 방식 (✓ 확정 / ? 추정)
slack-hangman ✅ 전체 ✅ 전체 ✓ user-level Bearer header
linear-davinci ✅ 전체 ✅ 전체 ✓ HTTP MCP (헤더 없음? — 별도 확인)
linear-hangman ✅ 전체 ✅ 전체 ✓ HTTP MCP
calendar ✅ 전체 ✅ 전체 ✓ env GOOGLE_OAUTH_CREDENTIALS=파일 경로
chrome-devtools ? (미확인) ✅ 전체 stdio (npx) — 환경변수 없음
gemini ? (미확인) ✅ 전체 ✓ workspace .mcp.json + API key
notion ⚠️ authenticate ✅ 전체 (notion-search/notion-fetch/notion-create-pages…) ? plugin OAuth, 토큰 위치 미확정

가설 3가지:

가설 A — OAuth 캐시 미전파 (Plugin runtime state)

가설 B — OAuth vs Integration Token 본질 차이

가설 C — config 누락 (단순 misconfiguration)

가장 강한 가설: A (plugin runtime state 미전파). B는 해결책으로는 유효하지만 원인 진단으로는 한 층 위. C는 거의 기각.

검증 방법은 §5에 모았다.

5. 재발 방지 운영안 + 검증 명령

A. 즉시 검증 (가설 A·B 좁히기)

가설 A·B를 가르는 1단계 검증:

# 1. Notion plugin이 인증 상태를 어디 저장하는지 후보 탐색
grep -rn "notion" ~/.claude/ 2>/dev/null \
  | grep -vE "(plugins/cache|history|projects/.*memory)" \
  | head -20

# 2. ~/.claude.json 안에 mcp 관련 키 외에 OAuth/token 저장 키가 있는지
jq 'keys' ~/.claude.json | grep -iE "(oauth|token|notion|auth|credential)"

# 3. plugin 디렉토리 안에 user-state 캐시가 있는지
find ~/.claude/plugins/cache/claude-plugins-official/notion -type f \
  \( -name "*.json" -o -name "*.tokencache*" -o -name ".env*" \)

후보 위치 확인되면 가설 A 확정. 후보 위치 없고 메인 세션 메모리에만 토큰이 있으면 가설 A의 더 강한 버전 (process-local only).

B. 빠른 해결책 (사고 직결 fix)

Notion Internal Integration Token을 발급해 user-level config에 박는 방향이 가장 작은 패치다.

# 1. Notion 사이트에서 Integration 생성: https://notion.so/profile/integrations
#    → "Internal" 타입, 권한: Read content / Update content / Insert content
# 2. workspace의 페이지·DB에 integration share
# 3. ~/.claude.json mcpServers에 추가 (전제: Notion MCP가 Bearer 헤더를 받는지 확인 필요)
#    또는 ~/Workspace/.mcp.json에 추가 (페르소나 cwd=~/Workspace 이므로 동일 효력)

⚠️ 전제 검증 필요: Notion plugin MCP가 Bearer <token> 헤더 또는 env: { NOTION_TOKEN: ... }을 받는지 확인. plugin 문서 또는 .mcp.json 스키마 확인 후에만 진행.

C. 일반 패턴 (재발 방지)

정책 내용
페르소나에 필요한 MCP는 config 본문에 토큰 명시가 default OAuth runtime cache 의존 MCP는 페르소나 측에서 예측 불가능한 실패 모드를 가진다. Integration Token / API Key 모드가 있는 MCP라면 그쪽으로 통일
OAuth만 지원하는 MCP는 페르소나 사용 가능 여부를 명시 문서화 가용 여부를 reference_pantheon_mcp_matrix.md (가칭) 같은 단일 표로 유지 — 이 보고서가 그 표의 초안
페르소나 spawn 시 MCP 헬스 체크 자동화 (장기) claude -p "list MCP tools available" 같은 진단 호출을 페르소나 부팅에 끼우고, 기대치와 다르면 alert
변경 시 메인 세션·페르소나 양쪽 검증 MCP 추가/변경 후 메인에서 정상 작동해도 페르소나에서 작동 안 할 수 있음. 두 환경 모두에서 1회 호출 검증

D. 페르소나-측 자기 진단 명령 (지니/완수가 직접 쓸 수 있는 것)

페르소나가 자기 deferred tool 목록을 점검할 수 있는 1줄:

# 페르소나 세션에서 ToolSearch로 후보 검색 후 deferred 목록과 대조
# 또는 사용자에게 직접 보고:
#   "내 MCP 도구 목록에 notion-search가 있나요? authenticate만 있다면 가설 A 재현."

이건 페르소나가 자기 환경을 메인 세션에 보고할 때 사용. 메인 세션이 두 환경을 매트릭스로 비교 가능.

신호 — 관측된 증거

시점 신호 출처
2026-06-07 지니 페르소나 측 Notion MCP에 mcp__notion__authenticate만 노출, notion-search/notion-fetch 부재 #northstar 콜로세움 트래커 스레드 (지니 발화)
2026-06-07 메인 Claude Code 세션은 동일 시점에 Notion MCP 도구 전체 정상 노출 본 세션 deferred tools (mcp__plugin_Notion_notion__notion-search 등)
2026-06-07 완수가 같은 작업(row fetch)을 본 세션에서 성공 — 페르소나 측이 아니라 메인 측 위임으로 #northstar 사고 스레드 (완수 발화)
2026-05-22 tools/slack_send.py로 페르소나 명의 Slack post — bot token이 config에 박혀 있어서 페르소나에서도 작동 reference_pantheon_slack_send (memory) — Bearer/API key 패턴은 페르소나에서 잘 작동하는 사례

결론

본 보고서의 핵심 주장 셋:

  1. 페르소나 LLM 인스턴스는 매 메시지마다 새로 spawn된다. 따라서 프로세스 시작 시 인증 가능한 MCP만 안정적이다. OAuth runtime 캐시 의존 MCP는 페르소나에서 예측 불가능한 실패를 보인다.
  2. Notion 단일 사고가 아니라 plugin MCP 인증 비대칭이라는 일반 패턴이다. 같은 패턴은 향후 다른 OAuth 기반 plugin MCP가 추가될 때마다 재현될 수 있다.
  3. 즉시 해결은 Integration Token을 config에 박는 가설 B 경로가 가장 작은 패치다. 단, Notion plugin이 Bearer/env 토큰을 받는지 먼저 검증해야 한다 (검증 못 하면 가설 B는 그림에 그친 떡).

권장 우선순위(지니가 사고 스레드에서 제시한 C → B → A 순서와 호응):

순위 액션 비용 효과
1 §5-A 검증 명령 실행 → OAuth 캐시 위치 확정 5분 가설 A vs B 결정
2 §5-B Integration Token 경로 가능성 확인 (Notion plugin 문서) 10분 가설 B 실행 가능성
3 가능하면 §5-B 실행 → 페르소나 측에서 notion-search 호출 1회 검증 30분 사고 직결 fix
4 §5-C 일반 패턴 문서화 — 본 보고서를 MCP 매트릭스의 단일 source로 보존 즉시 재발 시 진단 비용 절감

검증 질문 — 사용자에게

§5의 진단 명령을 돌려 본 다음, 사용자(또는 완수가 위임받아) 가르마를 타야 할 결정 3가지:

  1. 가설 A vs B 검증 결과가 나오면, Integration Token 경로로 가는 것이 페르소나 전반의 인증 정책으로 적절한가? OAuth(사용자 grant)와 Integration Token(워크스페이스 통합)은 행위 주체가 다르다 — 페르소나가 항승 본인 명의로 Notion 페이지를 만드는 것이 허용되는가, 아니면 페르소나 명의 별도 integration까지 가야 하는가?
  2. 페르소나가 자기 도구 목록을 사용자에게 보고하는 패턴을 일상화할 것인지. "지니, 너 deferred tools에 notion-search 있어?" 같은 점검을 세션 시작 시 자동으로 둘 것인지, 필요할 때만 사용자가 묻는 것으로 둘 것인지.
  3. plugin MCP는 페르소나에서 deprecated로 가는 정책을 채택할 것인지. OAuth runtime 캐시 의존 MCP는 메인 세션 전용으로 두고, 페르소나는 Bearer/API key 기반 MCP만 노출하는 룰. (Notion은 향후 Integration Token 경로가 열린다면 페르소나도 OK.)

모름·확인 필요

부록 — 핵심 파일 인용

pantheon/providers/claude.py:28
  self.cwd = cwd or str(Path(__file__).resolve().parents[2])
  # = ~/Workspace

pantheon/providers/claude.py:62-68
  cmd = ["claude", "-p", prompt,
         "--output-format", "stream-json", "--verbose",
         "--model", model or self.model,
         "--permission-mode", self.permission_mode]
  if session_id:
      cmd.extend(["--resume", session_id])

~/Workspace/.mcp.json
  { "mcpServers": { "gemini": { ..., "env": { "GEMINI_API_KEY": "..." } } } }

~/.claude.json mcpServers (요약)
  - chrome-devtools  : stdio (npx), env 없음
  - calendar         : stdio (npx), env GOOGLE_OAUTH_CREDENTIALS=파일경로
  - slack            : http (mcp.slack.com), oauth (clientId만)
  - linear-davinci   : http (mcp.linear.app), 헤더 없음
  - linear-hangman   : http (mcp.linear.app), 헤더 없음
  - slack-hangman    : http (mcp.slack.com), Bearer xoxp-...

~/.claude/settings.json enabledPlugins
  - notion@claude-plugins-official       ← 사고 케이스
  - code-review@claude-plugins-official

~/.claude/plugins/cache/claude-plugins-official/notion/0.1.0/.mcp.json
  { "mcpServers": { "notion": { "type": "http",
                                "url": "https://mcp.notion.com/mcp" } } }
  ← 토큰 슬롯 없음 (그래서 OAuth runtime cache 의존)

참조