Skip to content

Commit 14da1a9

Browse files
JoyDaheeChaclaude
andauthored
2주차 컨텍스트 엔지니어링 + 3주차 루프 (#52)
* fix: launchd/crontab 절대경로 제거로 clone 이식성 확보 레포에 박혀 있던 절대경로(/Users/joy/...)를 걷어내고, 설치 시점에 현재 clone 경로와 로그인 사용자명으로 자동 구성하도록 변경. - launchd/install.sh 추가: plist를 heredoc으로 ~/Library/LaunchAgents에 생성·로드 + remind-cron crontab 멱등 등록. 기존 심링크를 rm -f로 끊고 실제 파일을 써서, 심링크를 통해 레포 plist가 되살아나는 것을 방지. - launchd/uninstall.sh 추가: 데몬 언로드 + plist 삭제 + crontab 정리. - com.joy.telegram-listener.plist 삭제: 절대경로가 커밋에 남지 않도록 install.sh 생성으로 대체. - restart-listener-on-change.sh: LABEL·LOG를 id -un / $(dirname) 기반으로 동적 계산 (하드코딩 제거, install.sh가 만든 Label과 일치). - README: "세션 밖 자동화 설치" 절 추가, 디렉터리 구조 갱신. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: 로컬 read-model 도입 — list/done 조회를 네트워크에서 분리 cache.sh(신규): notion.sh read(curl 동기 ~0.5s)를 매번 타던 조회를 로컬 read-model로 뺀다. stale-while-revalidate — 캐시가 신선하면 ~10ms 즉시 반환, 오래되면 백그라운드 갱신. read-model = 순수 Notion 스냅샷 + 오버레이 2장(읽기 시점 _emit 한 곳에서 병합): - outbox 오버레이: 아직 동기 안 된 "생성"(방금 캡처) 항목을 스냅샷 위에 얹어 즉시 노출. - pending-done 오버레이: 아직 Notion 확인 안 된 "완료"를 낙관적으로 done 표시. 스냅샷은 순수 유지 → 백그라운드 refresh와 done이 서로 덮어쓰는 레이스 원천 차단. list-view.sh: 항목 조회를 notion.sh 직접 호출 → cache.sh read로 전환. 상태 필터는 전체 반환 후 jq 단에서 거른다. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf: capture 응답 속도 개선 — 텔레그램 노티스 <2s, 저장 비동기화 텔레그램 /capture: 기본 케이스의 claude -p 콜드스타트(실측 10.5s)를 우회하고 결정론 bash 경로로 처리(~0.03s). telegram-listener에 /capture) 케이스 추가(/list와 동일 패턴). 보안 3종(chat_id·명령 화이트리스트·eval 미사용) 유지 — 인자로만 전달. - classify.sh(신규): 사전 기반 카테고리 분류(모델 0). bash 3.2 호환, "가장 긴 매칭 키워드 우선" 규칙으로 부분매칭 모호성(약속 vs 약) 완화. - capture.sh(신규): 입력→분류→저장→노티스 결정론 엔트리. - capture-fast.sh(신규): 동기 Notion write(~0.48s) 대신 로컬 outbox 즉시 저장 + detached 백그라운드 동기. 실패 시 outbox 잔류 → capture-flush.sh(신규) 재시도(유실 방지). - capture/SKILL.md: 분류 서브에이전트 제거, 저장 비동기화, 두 실행 경로(대화형 메인 인라인 분류 vs 텔레그램 classify.sh 사전 분류, 저장은 capture-fast.sh 공유) 문서화. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: done 완료 낙관 반영 — 완료 즉시 반영, Notion은 백그라운드 done-fast.sh(신규): 완료 처리를 read-model 위에서 낙관적으로 즉시 반영한다. cache.sh mark-done으로 pending-done 오버레이에 id를 넣어 /list·/done에 곧바로 done으로 보이게 하고, Notion status 업데이트는 백그라운드로 던진다. → 네트워크 대기 없이 완료 UX. done/SKILL.md: 위 흐름에 맞춰 절차 갱신. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: CRUD 스모크 + 커버리지 스크립트 추가 tests/smoke_crud.sh: capture→list→plan→done 흐름의 로컬 스모크 테스트. tests/coverage.sh: 스킬·훅·데이터 파일 대비 테스트 커버리지 점검. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: state-sync-writer 공유 에이전트 README에 반영 sync-readme 스캔 결과 실제 존재하는 _shared/state-sync-writer.md가 문서에서 누락돼 있어, 에이전트 종류 표·디렉터리 트리·공유 설명에 등록. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: done 스킬에 cold-path 프리워밍·정직한 한계 설명 추가 캐시가 없는 cold 경우에만 타던 동기 네트워크(~0.68s)를 SessionStart 훅의 프리워밍으로 제거하는 방식과, 모델 추론 턴이라는 물리적 바닥으로 end-to-end 1초 보장이 불가능하다는 한계를 함께 문서화. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: 할일 런타임 데이터 gitignore 추가 tasks.json·clarified-specs.json·cache/·outbox/ 은 사용자 개인 할일 데이터와 로컬 캐시라 커밋 대상이 아님. secret·로그류를 개별 무시하던 기존 규칙과 같은 맥락으로 무시 처리. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: 정본 컨텍스트 도입 — .claude/context 5종 + 스킬별 참조 연결 같은 지식이 스킬·서브에이전트·셸 스크립트에 복제돼 하나 바꾸면 전부 손봐야 하던 문제를 정본(Single Source of Truth)으로 해소한다. - .claude/context 신설 ("읽히는 지식" 전용): data-model / categories / status-lifecycle / design-principles / security - 연결 방식 A(스킬별 참조): 각 SKILL.md·서브에이전트 상단에 참조 블록 추가 → 해당 스킬 실행 시에만 로드, 평상시 토큰 비용 없음 - capture SKILL의 잘못된 "분류 단일 출처" 서술을 categories.md로 정정 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: security 정본을 Eager 주입으로 승격 — CLAUDE.md @import "안 읽으면 사고"가 나는 안전 정본만 무조건 보장이 필요하므로, Lazy(스킬별 Read)에서 Eager(상시 주입)로 승격한다. 나머지 4개 정본은 Lazy 유지(하이브리드). - CLAUDE.md: @.claude/context/security.md 상시 주입 + "상시 주입 정본" 섹션 - remind/SKILL.md: security를 Lazy Read 목록에서 제외, 상시 주입됨을 명시(중복 Read 방지) - context/README.md: 연결 방식을 하이브리드로 갱신(로딩 컬럼 + 승격/강등 기준) 검증: 새 헤드리스 세션에서 security 불변 규칙은 컨텍스트에 로드됨(본문 그대로 인용), status-lifecycle 본문은 미로드 — Eager/Lazy 대조 확인 완료. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: done 스킬 SKILL.md 슬림화 — 배경 설명을 NOTES.md로 분리 SKILL.md는 매 실행 컨텍스트에 로드되므로 실행에 필요한 지시만 남기고, 속도 최적화의 "왜"(조회 캐시화·완료 크리티컬 패스 제거 등 배경 설명)는 런타임 미로드 문서 NOTES.md로 뺐다. 참조 정본도 로직 수정·디버깅 때만 Read. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: 컨텍스트 주입 도식(context-map.md) 신규 — 주입 시점 중심으로 작성 - "언제 무엇이 컨텍스트 창에 들어오나"를 시간축(시퀀스)·순간별 스냅샷으로 도식화 - Eager(security.md)/Lazy(4종)를 시점·주체·상주기간으로 대비 - §2에 스킬·에이전트별 참조 정본 표 + grep 재검증 팁 수록 - 주입 한 주제에 집중 (폴더 성격·지식그래프·갱신규칙은 README 소관) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: 컨텍스트 주입검증 테스트(inject.sh) 추가 — 정본 배선 무결성 L1 게이트 정본 컨텍스트가 실제 "주입 배선"에 걸려 있고 문서와 현실이 일치하는지 결정적으로 검증한다. lint.sh(파일 무결성)가 못 보는 배선 무결성을 커밋 전에 잡는다. - 검사 5종: Eager @import 존재·비어있지 않음(0바이트 빈 주입 포함), 고아 정본, context-map 소비자표 ↔ 실제 참조 양방향 드리프트, README Eager 표기 정합성 - 소비자는 하드코딩 대신 실제 파일 존재로 동적 해석 → 표에 가짜 행을 넣어 검증을 우회하는 것을 차단하고, 해석 불가한 이름은 건너뛰지 않고 실패시킨다 - 표 파서가 깨지면(헤더/열 변경) 무더기 오탐 대신 "파서 못 찾음/열 불일치"로 명확히 실패 - run-tests.sh L1 게이트에 편입(CI l1+l2 자동 포함), tests/README에 계층·알려진 한계 문서화 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: sync-readme 스캔을 state-scanner에 위임 — 메인 컨텍스트 흡수 89%↓ sync-readme가 README 전문(13.7KB)과 디렉터리 트리를 메인 컨텍스트에서 직접 Read하던 것을, 읽기 전용 공유 서브에이전트 state-scanner에 위임한다. 스캐너가 자기 컨텍스트에서 스캔·비교를 흡수하고, 메인에는 압축된 "사실 + 불일치" 두 블록만 반환한다. - 신규 _shared/state-scanner.md: writer와 대칭인 수집 담당 공유 서브에이전트. 입력 계약(스캔 지시·비교 대상·도메인 관심사) + 불변 규칙(읽기 전용, 원자료 통째 반환 금지, 비밀값 존재만). "반환 텍스트가 곧 산출물" 명시로 빈 응답(스텁) 방지. - sync-readme Step1·2(스캔+README Read)를 스캐너 위임으로 교체 → 메인 흡수 ~5,108토큰 → ~513토큰(89%↓). writer가 어차피 자기 컨텍스트에서 README를 다시 읽으므로 메인이 중계할 필요가 없다는 게 핵심. - context-map 소비자표에 state-scanner 행 추가(정본 참조 design-principles 1종). 측정 근거: OLD=스캔4177B+README13703B, NEW=스캐너 압축 반환 1797B. sync-test는 grep-only(스캔 271토큰)라 위임 시 서브에이전트 비용이 이득을 초과 → 의도적으로 미적용(위임 손익분기: 메인 절감 × 잔존시간 vs 서브에이전트 컨텍스트 비용). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: Eager 컨텍스트 예산 게이트(context-budget.sh) 추가 — 상주 정본 크기 회귀 차단 Eager 정본(CLAUDE.md + @import 대상)은 스킬 실행과 무관하게 매 세션 항상 컨텍스트 창에 상주하므로, 여기가 커지면 모든 세션이 그 비용을 영구히 부담한다. "정말 모든 상황에서 필요한가"를 통과 못 한 내용이 슬그머니 Eager로 승격되는 회귀를 L1에서 잡는다. - @import 목록을 하드코딩하지 않고 CLAUDE.md에서 동적 파싱 → 새 Eager import 자동 포함 - 주 게이트=근사 토큰(EAGER_MAX_TOKENS 기본 1200), 보조=줄 수(EAGER_MAX_LINES 기본 80) - 초과 시 상한 조정이 아니라 "Lazy로 내릴 내용 재심사"를 유도하는 실패 메시지 - run-tests.sh L1에 편입(lint+inject+budget), tests/README에 inject와의 역할 분담 문서화 - inject.sh(배선 존재)와 상보: budget은 그 배선의 페이로드 크기를 본다 현재 Eager 50줄·783토큰(상한 내). 음성테스트(상한 100→exit 1)로 게이트 실동작 확인. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: sync-readme를 단일 서브에이전트(readme-sync-agent)로 통합 — 총 토큰 반감 sync-readme가 scanner→writer 두 서브에이전트를 거치던 것을, 스캔·비교·갱신을 한 창에서 끝내는 readme-sync-agent 한 곳에 위임하도록 통합한다. 서브에이전트를 두 번 열면 메인은 가벼워져도 총 토큰이 오히려 늘고(직전: scanner~42k + writer~40k ≈ 82k), README 갱신은 "스캔한 사실을 그대로 옮기는" 단순 전사라 수집·작성을 나눌 실익이 없었다. - 신규 _shared/readme-sync-agent.md: 스캔+대상 Read+최소 diff 갱신+요약 반환을 자기 창에서 전부 수행. 원자료를 메인으로 중계하지 않아 총 토큰 ~42k, 메인 흡수 수십 토큰. - _shared/state-scanner.md 삭제(통합으로 대체). state-sync-writer는 유지 — 사람이 중간에 "자동 채움/보고만"을 결정하는 sync-test가 여전히 수집·작성 분리를 필요로 하기 때문. - context-map §1-3/§1-4/§2를 readme-sync-agent로 정정. §1-4에 "위임은 서브에이전트 창을 통째로 여는 비용이 있다"는 한계를 명시(89%↓만 강조하던 서술 교정) — 메인이 큰 파일을 Read할 때만, 또 창을 한 번만 열 때만 이득임을 남긴다. - state-sync-writer/sync-test 문서의 "sync-readme도 재사용" 서술 정정. 검증: L1(lint·inject·budget) + L2 = 33+9+2+13 pass / 0 fail. state-scanner 잔존 참조 0건. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: 컨텍스트 주입 효과 A/B 실험(ab-injection) + 미션 결과 리포트 추가 inject.sh(배선 존재)와 상보적으로, 정본 주입이 스킬 "동작"을 실제로 바꾸는지를 통제 실험으로 측정한다. 분류 정본 categories.md 주입 有無(Arm A/B)를 같은 모델· 같은 입력 12종·각 3회(72관측)로 대조. - tests/ab-injection/: 정답셋(ground-truth.tsv)·원자료(raw-runs.tsv)·재현 채점기 (score.sh)·설계/결과 문서(README.md). 채점은 손계산이 아닌 TSV 재계산이라 검증 가능. 결과: 정확도 50.0%→97.2%, 6종 어휘 준수 50.0%→100%, 3회 일관성 58.3%→91.7%, 함정 항목 22.2%→88.9%. 미주입 arm은 스키마 밖 라벨 8종을 지어내 list-view 그룹핑을 깸. - tests/README.md: ab-injection을 "게이트 아닌 1회성 효과 측정"으로 인덱스에 연결 (inject=배선 존재 / ab-injection=효과, 상보 관계 명시). - docs/mission-2-report.md: 미션 5개 과제(필수 3+도전 2) 과제별 판정·근거 리포트. GitHub 웹 렌더링용 마크다운(유니코드 막대로 A/B 시각화, mermaid 미지원 환경도 무해). 검증: L1(lint 35 + inject 9 + budget 2) pass / 0 fail. 리포트 상대링크 7개 실재 확인. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: 스킬 호출 로깅 훅 추가 + 훅 배선을 tracked settings.json으로 승격 자기 관찰 루프의 write 절반을 배선한다. 그동안 skill-invocations.log는 security.md 정본에 등록만 돼 있고 실제로 기록하는 코드가 없어 6/25 이후 멈춰 있던 고아 로그였다. PostToolUse(matcher=Skill) 훅으로 스킬 실행 시마다 "시각 | #N | 스킬명" 한 줄을 append 해 로그를 되살린다. 동시에 구조적 틈을 메운다: 훅 스크립트는 커밋돼 있었지만 이를 활성화하는 배선(hooks 설정)은 gitignore된 settings.local.json 에만 있어 clone 시 재현되지 않았다. 공유 hooks 블록을 tracked settings.json 으로 승격하고, 개인 permissions 만 settings.local.json 에 남긴다(Claude Code가 둘을 병합). 이제 OS 배선이 repo 안에서 재현된다 — CLAUDE.md의 "OS 파일은 프로젝트 안에" 원칙에 부합. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: pre-commit 드리프트 방지 게이트 추가 (재현 가능한 git 훅 배선) 매 커밋 직전에 결정적 테스트(run-tests.sh l1 l2, ~1초)를 돌려, 정본이 실제 상태와 어긋난 채 커밋되는 것을 구조적으로 막는다. L1 이 끊어진 참조 링크 (lint) · 정본 주입 배선 끊김/고아 정본/지도 드리프트(inject) · Eager 상주 정본 크기 회귀(context-budget)를 잡고, L2 가 detect-todo.js 계약을 검증한다. 자격증명·LLM 이 필요한 L3(smoke)는 게이트에서 제외 — 느리고 비결정적이라 매 커밋 게이트엔 부적합. 재현성 — .git/hooks 는 버전 관리가 안 돼 클론 시 훅이 사라진다. 훅을 추적되는 .claude/githooks 에 두고 git 의 core.hooksPath 로 연결한다. 클론한 사람은 'bash .claude/githooks/install.sh' 한 번이면 게이트가 산다(uninstall.sh 로 해제). settings.json 승격과 같은 "배선을 repo 안에서 재현" 원칙. 검증 완료: 고아 정본을 만들어 커밋 시도 → L1 FAIL 로 차단, HEAD 불변 확인. 긴급 우회는 git 네이티브 --no-verify 로 가능. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: /usage 스킬 추가 — 자기 관찰 루프의 read 절반 log-skill-invocation.sh(write 절반)가 쌓은 skill-invocations.log 를 읽어 사용 패턴을 뽑는다. 이로써 "관찰 → 개선" 루프가 닫힌다. usage-report.sh(결정론적 공유 스크립트)가 세 가지를 뽑는다: ① 빈도 — 자주 쓰는 스킬 순위 (별칭·고속화 후보) ② 연쇄 — 바로 이어 부른 쌍(capture→plan 등), 임계 이상 반복 시 콤보 제안 ③ 유휴 — 등록됐지만 호출 이력 없는 스킬 (폐기·점검 후보) /usage 스킬은 이 스크립트를 1회 실행하고 출력을 verbatim relay 한다 (/skills·/remind-when 과 같은 "정본 직독 + 결정론 조회" 패턴). 정직한 한계: 로그에 성공/실패 필드가 없어 "실패 반복"은 감지 못 한다. 원하면 write 훅이 tool_response 상태까지 남기도록 스키마를 넓혀야 한다(후속). 검증: 실제 로그(2줄)→"표본 얕음" 경고, 풍부한 합성 로그(11줄)→빈도·연쇄· 콤보 제안·유휴 세 섹션 모두 정확 출력 확인(테스트 후 로그 원상복구). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: README를 실제 상태에 동기화 (/usage·새 훅·githooks 반영) /sync-readme 로 파일시스템을 스캔해 이번 세션의 추가분을 README에 반영: - 스킬 표에 /usage 추가 - 에이전트 표에 README Sync 추가, State-Sync Writer 범위를 sync-test 전용으로 정정 - 훅 표에 log-skill-invocation(PostToolUse matcher Skill) 추가 - 디렉터리 트리에 settings.json·skill-invocations.log·githooks/·usage/· readme-sync-agent.md·usage-report.sh 등록 최소 diff, 기존 한국어 톤 유지. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: 자기 관찰 스크립트 결정적 단위 테스트 추가 (L2) /sync-test 로 커버리지 빈틈을 스캔한 결과, 이번 세션에 추가된 순수 결정적 스크립트 두 개가 무테스트였다. telegram-listener·remind-cron 같은 외부 의존 .sh 와 달리 이 둘은 네트워크·데몬·claude 의존이 전혀 없어(stdin→append / 로그→stdout) detect-todo.js 처럼 단위 검증이 가능하다. - tests/unit-scripts.sh 신규: log-skill-invocation(append·카운터 증가·비-Skill 스킵·네임스페이스 보존)과 usage-report(빈도·연쇄·유휴·빈 로그) 계약 검증. - 테스트 격리를 위해 두 스크립트에 SKILL_LOG 환경변수 오버라이드 추가 — 임시 로그를 주입해 실데이터(skill-invocations.log)를 건드리지 않는다. 기본 동작은 불변. - run-tests.sh L2 에 배선(CI L1+L2 가 자동 커버), tests/README.md 문서화. 검증: run-tests.sh l1 l2 통과(L2 13+8 pass), 실로그 무접촉 확인. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: 시스템 루프 3종 추가 — outbox 재동기·데몬 감시·주간 집계 세션 밖에서 도는 자동화 루프 3개를 crontab 으로 배선한다(remind 와 같은 방식). 1) outbox 재동기 (조정 루프 완성) — flush-cron.sh, 15분마다 capture-flush.sh 를 주기 구동해 outbox 미동기 항목을 재전송, "모두 동기됨" 상태로 수렴. 몸통만 있고 자동 구동이 없던 조정 루프의 나머지 절반. 2) 감시 (watchdog) — watchdog-cron.sh, 10분마다 telegram-listener 데몬 헬스 체크. 다운 감지 시 폰 알림 + 자동 재시작 시도. 상태 파일로 healthy↔down 전이에만 알림(디바운스). launchd KeepAlive 가 못 잡는 "언로드/미기동"을 사용자에게 알린다. 3) 집계 (digest) — digest-cron.sh, 매주 일요일 20:00 digest-report.sh(결정론 집계)로 상태 분포·카테고리·2일+ 방치 draft 를 요약해 폰으로 발송. remind 가 매일 재촉이라면 이건 주 1회 회고. 공유·인프라: - _shared/telegram-send.sh: 알림 루프 공용 sender (security.md 규칙 준수 — 값 노출 없이 실패, 자격증명 없으면 미발송). - launchd/install.sh·uninstall.sh: cron 4종 멱등 등록/제거. - .gitignore·security.md 정본: 새 런타임 상태·로그 파일 등록(양쪽 일치). 테스트·검증: - unit-scripts.sh 에 digest-report(집계 로직)·telegram-send(빈 메시지 가드) 결정론 테스트 추가(ITEMS_JSON_FILE 픽스처 주입). run-tests l1 l2 통과(14 pass). - 게이트가 inject.sh 검사4 로 telegram-send 의 미문서화 정본 참조를 잡아내, .sh 는 §2 주입 지도의 소비자가 아니라는 관례에 맞춰 경로 인용을 제거해 해소. - README·tests/README 를 실제 상태에 동기화. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: 설계 원칙에 §7 저장소 3분류(캐시·버퍼·축적) 정본 추가 로컬-우선(§2)이 데이터를 여러 로컬 파일에 쌓지만 "로컬에 쌓인다"가 같다고 캐시·버퍼·축적을 뭉뚱그리면 안 된다 — 정상 상태가 정반대다. 저장소를 만지기 전에 셋 중 무엇인지 목표 상태로 판정하는 기준을 정본화한다. - 캐시(원본과 일치·재생성 무해) / 버퍼(0이 목표·지우면 유실) / 축적(끝없이 성장) 을 표로 레포 실체에 매핑. - 루프도 같은 축으로 분리: 자율탐지형(조정 — flush·watchdog·digest) vs 자산 축적형(자기 관찰 루프 = 로그 write ↔ /usage read). - 혼동 방지: capture 로컬 저장부는 캐시+버퍼이지 축적이 아니며, outbox 는 데이터일 뿐 루프는 그걸 드레인하는 flush-cron(탐지형)임을 명시. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: 문서를 docs/ 로 이동·정리 + 시스템 루프 다이어그램 추가 - context-map.md 를 .claude/context/ → docs/ 로 이동하고 README 링크 보정 - mission-2-report.md 의 context-map 링크를 새 위치 기준으로 보정 - system-loops.md 신규: 세션 밖 시스템 루프 6종(cron 4·데몬·자기관찰)의 동작 순서를 탐지형/축적형 축으로 정리한 mermaid 다이어그램 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: inject.sh 드리프트 검사 경로를 docs/context-map.md 로 갱신 context-map.md 를 docs/ 로 옮기면서 §4 드리프트 검사가 파일을 못 찾아 조용히 SKIP 되던 것을 되살린다. 검사 8→9 pass 로 복귀. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 209df38 commit 14da1a9

59 files changed

Lines changed: 3542 additions & 209 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/context/README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# .claude/context — 정본(Single Source of Truth) 컨텍스트
2+
3+
이 폴더는 **여러 스킬·서브에이전트가 공통으로 참조하는 "사실"을 한 곳에 모은 정본**이다.
4+
같은 지식이 스킬·에이전트·셸 스크립트에 복붙되어 "하나 바꾸면 전부 손봐야 하는" 문제를
5+
없애기 위해 존재한다.
6+
7+
## 폴더 성격 구분
8+
9+
이 프로젝트의 `.claude` 하위는 성격으로 나뉜다. 헷갈리지 않게 정리한다.
10+
11+
| 폴더 | 성격 ||
12+
|------|------|-----|
13+
| `skills/_shared/*.sh` | **실행되는 것** (코드) | `classify.sh`가 돌아감 |
14+
| `skills/_shared/*.md` | **호출되는 것** (에이전트 프롬프트) | `classifier-agent.md`를 Agent 도구가 prompt로 씀 |
15+
| **`context/*.md`** | **읽히는 것** (참조 사실) | `data-model.md`를 스킬이 Read해서 참고 |
16+
17+
## 정본 목록
18+
19+
| 파일 | 담는 것 | 로딩 |
20+
|------|---------|------|
21+
| [`data-model.md`](./data-model.md) | todos 스키마 + 저장 구조(캐시·outbox·자격증명 경로) | Lazy |
22+
| [`categories.md`](./categories.md) | 카테고리 6종 정의 + 분류 규칙 | Lazy |
23+
| [`status-lifecycle.md`](./status-lifecycle.md) | 상태 전이(draft→planned→done) + 스킬별 담당 | Lazy |
24+
| [`design-principles.md`](./design-principles.md) | 오케스트레이터 패턴·로컬우선동기 등 설계 어휘 | Lazy |
25+
| [`security.md`](./security.md) | 비밀값 취급 규칙 + 민감 파일 목록 | **Eager** |
26+
27+
## 연결 방식 (하이브리드 — Lazy 기본 + 안전 정본만 Eager)
28+
29+
정본은 두 방식으로 주입된다. 파일 성격에 따라 나뉜다.
30+
31+
### Lazy (기본) — 필요할 때만 Read
32+
33+
대부분의 정본은 **각 스킬/서브에이전트가 필요할 때 Read**한다. CLAUDE.md에 항상 주입하지
34+
않으므로, 해당 스킬을 실행할 때만 로드되어 평상시 토큰 비용이 없다. 단, 로드 트리거가
35+
모델의 지시 준수라 100% 보장은 아니다(모델 주도 Lazy 로딩).
36+
37+
각 SKILL.md / 서브에이전트 프롬프트 상단에는 아래처럼 참조가 걸려 있다.
38+
39+
```markdown
40+
> **참조 정본**: 이 스킬은 아래 정본을 따른다. 관련 판단 시 먼저 Read한다.
41+
> - `.claude/context/data-model.md`
42+
> - `.claude/context/status-lifecycle.md`
43+
```
44+
45+
### Eager (안전 정본) — CLAUDE.md로 상시 주입
46+
47+
`security.md`처럼 **"안 읽으면 사고"가 나는, 무조건 보장이 필요한** 정본은 프로젝트
48+
`CLAUDE.md`에서 `@import`로 매 세션 항상 로드한다. 특정 스킬 실행과 무관하게 컨텍스트에
49+
있으므로, 각 스킬은 이를 별도 Read하지 않는다.
50+
51+
```markdown
52+
<!-- 프로젝트 CLAUDE.md -->
53+
@.claude/context/security.md
54+
```
55+
56+
> **승격/강등 기준**: "빈도가 높다"만으로 Eager로 올리지 않는다(길면 매 세션 토큰 낭비).
57+
> "안 읽었을 때의 리스크가 크고 + 파일이 짧다"가 Eager의 조건이다.
58+
59+
## 규칙
60+
61+
- 정본을 바꿀 때는 **이 폴더의 파일을 먼저** 고친다. 그다음 그 정본의 "코드 표현"
62+
(예: `classify.sh`)이나 요약이 어긋났으면 최소 diff로 맞춘다.
63+
- 새 정본을 추가하면 이 README의 목록과, 참조하는 스킬의 상단 블록을 함께 갱신한다.

.claude/context/categories.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# 분류 규칙 정본 — 할일 카테고리 6종
2+
3+
> **이 파일의 역할**: 할일 `category`의 유일한 출처. 지금까지 이 규칙은 세 곳
4+
> (capture SKILL Step 2 표 · `_shared/classifier-agent.md` · `_shared/classify.sh`)에
5+
> 복제돼 있었다. 이제 **이 파일이 정본**이고, 나머지는 이 파일을 참조하거나(md) 이 파일의
6+
> 코드 표현(bash)으로 취급한다. 규칙을 바꾸면 이 파일을 먼저 고친다.
7+
8+
## 카테고리 정의
9+
10+
| 카테고리 | 해당 키워드 예시 |
11+
|---------|---------------|
12+
| **스터디** | 강의, 책, 자격증, 세미나, 공부, 학습, 과제, 읽기 |
13+
| **업무** | 미팅, 리포트, 데드라인, 리뷰, 회의, PR, 발표, 제안서 |
14+
| **일상** | 장보기, 청소, 약속, 취미, 쇼핑, 요리, 빨래 |
15+
| **건강** | 병원, 운동, 약, 검진, 헬스, 산책, 수면 |
16+
| **금융** | 납부, 이체, 세금, 보험, 카드, 환전, 저축 |
17+
| **기타** | 위 카테고리에 해당하지 않는 것 |
18+
19+
`category` 필드에는 이 6개 값 중 **하나의 한글 단어**만 들어간다.
20+
다른 값을 쓰지 않는다 → 스키마는 [`data-model.md`](./data-model.md).
21+
22+
## 분류 규칙
23+
24+
1. **가장 가까운 카테고리 하나**를 고른다.
25+
2. **두 카테고리에 걸치면 더 구체적인 쪽**을 고른다.
26+
(예: "헬스장 등록비 납부" → 금융이 아니라 **건강**)
27+
3. **확실하지 않으면 `기타`**.
28+
4. 분류 결과에 **설명을 덧붙이지 않는다** — 카테고리 이름 한 단어만.
29+
30+
## 이 규칙을 쓰는 세 표현 (모두 동기화 대상)
31+
32+
같은 규칙이 실행 경로에 따라 세 가지로 나타난다. 하나를 바꾸면 셋을 함께 갱신한다.
33+
34+
| 표현 | 위치 | 언제 쓰나 |
35+
|------|------|-----------|
36+
| **이 파일** | `context/categories.md` | 정본 — 규칙의 기준 |
37+
| 메인 인라인 분류 | `capture/SKILL.md` Step 2 | 대화형 `/capture` (메인 모델이 표 보고 직접) |
38+
| 서브에이전트 프롬프트 | `_shared/classifier-agent.md` | Agent 도구로 분류 위임 시 |
39+
| bash 사전 분류 | `_shared/classify.sh` | 텔레그램 경로 (모델 없이 ~0.03s) |
40+
41+
> **왜 bash 표현이 따로 있나**: 텔레그램 경로는 `claude -p` 콜드 스타트(~10s)를 우회해야
42+
> 노티스가 2초 안에 나간다. 그래서 모델 대신 사전으로 분류한다. 대표 키워드는 정확하지만
43+
> 한 글자 모호어(예: "약")는 놓칠 수 있는 의도된 트레이드오프이며, 나중에 `/plan`에서
44+
> 카테고리를 조정할 수 있다. 설계 배경은 [`design-principles.md`](./design-principles.md).

.claude/context/data-model.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# 데이터 정본 — todos 스키마 & 저장 구조
2+
3+
> **이 파일의 역할**: 이 OS가 다루는 유일한 데이터인 "할일(todo)"의 형태와 저장 위치를
4+
> 한 곳에 못 박는다. capture·plan·done·list·remind 는 전부 이 데이터를 읽고 쓰므로,
5+
> 스키마가 궁금하면 각 스크립트를 역추적하지 말고 **이 파일 하나**를 본다.
6+
> (연결 방식 = 각 스킬이 필요할 때 이 파일을 Read — `context/README` 참고)
7+
8+
## 1. 할일(todo) 스키마
9+
10+
할일 1건은 아래 필드를 가진 flat JSON 객체다.
11+
12+
| 필드 | 타입 | 필수 | 의미 |
13+
|------|------|:---:|------|
14+
| `id` | string (uuid) || 고유 식별자. 로컬 생성 시 임시 id, Notion 반영 후 page id 부여 |
15+
| `title` | string || 할일 제목 (캡처 키워드 그대로) |
16+
| `category` | enum || 6종 중 하나 → [`categories.md`](./categories.md) |
17+
| `status` | enum || `draft` \| `planned` \| `done`[`status-lifecycle.md`](./status-lifecycle.md) |
18+
| `captured_at` | ISO8601 || 캡처 시각. 한국 시간대(`+09:00`). 스크립트가 자동 기록 |
19+
| `recurrence` | null \| string | | 반복 규칙. `null`(1회) \| `"once"` \| `"daily"`|
20+
| `due_date` | null \| date(YYYY-MM-DD) | | 마감일. 미정이면 `null` |
21+
| `time` | null \| string | | 시각/시간대 (예: `"저녁"`, `"09:00"`). 미정이면 `null` |
22+
| `detail` | null \| string | | 구체화 내용. `"이유: ...\n방법: ..."` 형식. plan 단계에서 채워짐 |
23+
24+
### 예시
25+
26+
```json
27+
{
28+
"id": "38b079ad-1f35-81e7-8b21-c872361b519e",
29+
"title": "장보기",
30+
"category": "일상",
31+
"status": "planned",
32+
"captured_at": "2026-06-25T10:00:00.000+09:00",
33+
"recurrence": null,
34+
"due_date": "2026-06-25",
35+
"time": null,
36+
"detail": "이유: 냉장고가 비었음\n방법: 마켓 직접 방문"
37+
}
38+
```
39+
40+
### 불변 규칙
41+
42+
1. **필드 직렬화는 flat JSON 배열** — 중첩 없이 위 필드만.
43+
2. **`detail`의 줄바꿈은 `\n` 문자열로 보존** — 저장 시 `echo` 금지, `printf '%s'` 사용
44+
(zsh `echo``\n`을 실제 개행으로 바꿔 JSON을 깨뜨린다).
45+
3. **`captured_at`은 사람이 손대지 않는다** — 캡처 스크립트가 채우는 값.
46+
47+
## 2. 저장 구조 (정본 = Notion, 로컬 = 캐시/버퍼)
48+
49+
원격 정본은 **Notion**이고, 로컬은 속도를 위한 read-model(캐시) + write 버퍼(outbox)다.
50+
"로컬-우선 + 백그라운드 동기" 설계는 [`design-principles.md`](./design-principles.md) 참고.
51+
52+
| 경로 | 성격 | 쓰는 주체 |
53+
|------|------|-----------|
54+
| `.claude/data/cache/todos.json` | **로컬 read-model** — list/done 조회의 출처 | `cache.sh`, `done-fast.sh` |
55+
| `.claude/data/cache/pending-done.json` | 완료 대기 큐 (백그라운드 동기용) | `done-fast.sh` |
56+
| `.claude/data/outbox/` | capture write 버퍼 (유실 방지, 재시도) | `capture-fast.sh`, `capture-flush.sh` |
57+
| `.claude/data/notion.json` | Notion 자격증명 **(비밀값·gitignore)** | 읽기 전용 → [`security.md`](./security.md) |
58+
| `.claude/data/telegram.json` | 텔레그램 자격증명 **(비밀값·gitignore)** | 읽기 전용 → [`security.md`](./security.md) |
59+
60+
### 읽기/쓰기 경로 요약
61+
62+
- **읽기(list·done·plan·remind)**: `cache.sh` / `notion.sh read` — 크리티컬 패스에 네트워크 없음.
63+
- **쓰기(capture)**: `capture-fast.sh` → outbox 즉시 + Notion 백그라운드.
64+
- **완료(done)**: `done-fast.sh complete` → 캐시 즉시 반영 + Notion 백그라운드.
65+
66+
> 캐시가 아예 없는 최초(cold)에만 동기 네트워크를 타며, SessionStart 훅(`cache.sh refresh-bg`)이
67+
> 미리 데워 이를 제거한다.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# 설계 원칙 정본 — 이 OS를 "이 OS답게" 만드는 판단 기준
2+
3+
> **이 파일의 역할**: 이 프로젝트가 반복해서 따르는 설계 어휘를 모은다. 데이터가 아니라
4+
> **"어떻게 만들지 판단하는 기준"**이다. 새 스킬을 짜거나, state-sync-writer·sync-readme·
5+
> sync-test 같은 문서/리팩터 서브에이전트가 코드를 만질 때 이 원칙과 어긋나지 않게 한다.
6+
7+
## 1. 오케스트레이터 패턴 — 스킬은 얇게, 일은 위임
8+
9+
SKILL.md는 **"무엇을 할지"만 결정하는 얇은 조율자**다. 실제 일(분류·저장·포맷·발송)은
10+
공유 스크립트(`_shared/*.sh`)나 서브에이전트(`_shared/*.md`)가 한다.
11+
12+
- 가벼운 판단(키워드 1개 → 카테고리 1개)은 **오케스트레이터가 흡수**한다.
13+
- 무거운 일(추론·대화가 필요한 인터뷰, 메시지 작성)만 **서브에이전트에 위임**한다.
14+
- 같은 책임은 **공유체 하나로 재사용**한다 — 예: 알림 발송은 `telegram-agent`, 정본 갱신은
15+
`state-sync-writer` 하나로. 채널/산출물이 바뀌어도 그 파일만 교체하면 된다.
16+
17+
## 2. 로컬-우선 + 백그라운드 동기 — 네트워크를 크리티컬 패스에서 뺀다
18+
19+
사용자 체감 응답에서 네트워크 왕복을 없애는 게 이 OS의 성능 축이다.
20+
21+
- **쓰기**: 로컬 outbox/캐시에 즉시 durable 저장(≈10ms) → 원격(Notion) 반영은
22+
detached 백그라운드. 유실 없음(실패 시 outbox에 남아 재시도).
23+
- **읽기**: 로컬 read-model(캐시)에서 즉시 반환 → stale이면 갱신을 백그라운드로.
24+
- **cold(캐시 없음)만 예외**로 동기 네트워크를 타며, SessionStart 훅이 미리 데워 제거한다.
25+
26+
구체적 경로는 [`data-model.md`](./data-model.md)의 저장 구조 참고.
27+
28+
## 3. 결정론적 호출은 LLM을 우회한다
29+
30+
입력이 정해지면 출력도 정해지는 일(조회·필터·상태 업데이트·표시 규칙)에는 서브에이전트를
31+
띄우지 않는다. 콜드 스타트·프롬프트 재독해·curl 조립 추론이 낭비이기 때문.
32+
33+
- 결정론적 → **Bash로 직접 호출** (`notion.sh`, `list-view.sh`, `done-fast.sh` …).
34+
- 비결정론적(추론·대화 필요) → **Agent 도구로 위임**.
35+
36+
## 4. 단일 출처(Single Source of Truth) + 최소 diff
37+
38+
- 같은 지식/규칙은 한 곳에만 둔다. 그래서 이 `context/` 폴더가 존재한다.
39+
표시 규칙은 `list-view.sh`, 분류 규칙은 [`categories.md`](./categories.md)에 모은다.
40+
- 정본을 갱신할 때는 **전면 재작성이 아니라 어긋난 부분만 최소 diff**로 고친다
41+
(state-sync-writer의 불변 규칙). 기존 구조·말투·한국어 톤을 유지한다.
42+
43+
## 5. 정직한 한계 서술
44+
45+
못 하는 것을 숨기지 않는다. 예: "저장을 백그라운드로 빼도, `/capture` 요청을 이해하고
46+
도구 호출을 발행하는 **모델 추론 턴**은 물리적 바닥이라 end-to-end 1초 보장은 불가능하다."
47+
스킬이 제어 가능한 범위(네트워크·모델 턴 수)를 최적화하되, 물리적 한계는 명시한다.
48+
49+
## 6. 비밀값은 절대 노출하지 않는다
50+
51+
자격증명(토큰·chat_id 등)은 gitignore된 파일에서만 읽고, 로그·응답·커밋에 전문을 쓰지
52+
않는다. 상세는 [`security.md`](./security.md).
53+
54+
## 7. 로컬 저장소는 "목표 상태"로 구분한다 — 캐시 · 버퍼 · 축적
55+
56+
§2의 로컬-우선 전략은 데이터를 여러 로컬 파일에 쌓는다. 그런데 "로컬에 쌓인다"가 같다고
57+
셋을 뭉뚱그리면 안 된다 — **정상 상태(steady state)가 정반대**이기 때문이다. 어떤 저장소를
58+
만지기 전에 "이건 셋 중 뭐냐"를 먼저 판정한다.
59+
60+
| 종류 | 목표 상태 | 지우면 | 레포 실체 |
61+
|------|----------|--------|-----------|
62+
| **캐시** | 원본과 일치하는 거울 | 재생성됨(무해) | `data/cache/todos.json` — "지워도 되는 순수 Notion 스냅샷" |
63+
| **버퍼** | **비어 있음(0)** | **유실됨** | `data/outbox/*.json`, `pending-done.json` — 아직 동기 안 된 생성·완료 |
64+
| **축적 저장소** | 끝없이 성장 | 과거가 사라짐 | `skill-invocations.log` — append-only, 커질수록 가치 |
65+
66+
- **캐시**는 원본(Notion)의 파생물이라 삭제해도 재생성된다. 목표는 "원본과 일치".
67+
- **버퍼**는 정반대로 **비어야 정상**이다. 값을 담고 있는 건 "아직 처리 못 함"의 표시일 뿐,
68+
성공하면 사라진다. 그래서 버퍼는 지우면 원본이 유실된다.
69+
- **축적 저장소**만 "커질수록 가치"다. 드레인하지 않고 계속 append 한다.
70+
71+
### 루프도 이 축으로 갈린다 — 탐지형 vs 축적형
72+
73+
- **자율탐지형(조정) 루프**: "이상/미처리 있나?"를 주기적으로 감지해 반응하고 잊는다.
74+
잘 돌수록 조용하다. 버퍼를 **0으로 드레인**하거나 데몬을 지키는 루프가 여기 속한다 —
75+
`flush-cron`(outbox 재동기), `watchdog-cron`(데몬 헬스), `digest-cron`(방치 draft 탐지).
76+
- **자산 축적형 루프**: 매 반복이 성장하는 저장소에 한 겹씩 쌓고, 그 축적물을 나중에 회수해
77+
더 큰 가치를 낸다. **write 절반과 read 절반이 쌍**을 이룬다 — 이 레포의 유일한 순수 예는
78+
**자기 관찰 루프**(로그 write 훅 → `/usage` read)다.
79+
80+
> 흔한 혼동: capture의 로컬 저장부는 **캐시 + 버퍼**이지 축적이 아니다. outbox는 목표가
81+
> "0"이라 축적과 정반대고, outbox 자체는 데이터일 뿐 루프는 그걸 드레인하는 `flush-cron`
82+
> (탐지형)이다. "로컬에 쌓인다"만 보고 축적형 루프라 부르지 않는다.

.claude/context/security.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# 비밀값 규칙 정본 — 자격증명 취급
2+
3+
> **이 파일의 역할**: 짧지만 치명적인 규칙. 자격증명을 만지는 모든 경로(telegram-agent,
4+
> notion.sh, 리스너, 디버깅 중인 서브에이전트)가 공통으로 지킨다. 지금까지 이 규칙은
5+
> telegram-agent.md 안에만 부분적으로 있었다 — 교차 관심사라 별도 정본으로 뺀다.
6+
7+
## 민감 파일 (모두 `.gitignore` 대상 — 커밋 금지)
8+
9+
| 파일 | 내용 |
10+
|------|------|
11+
| `.claude/data/telegram.json` | 텔레그램 봇 토큰, chat_id |
12+
| `.claude/data/notion.json` | Notion API 자격증명 |
13+
| `.claude/data/telegram-offset.txt` | 리스너 폴링 오프셋 (로컬 런타임) |
14+
| `.claude/data/telegram-listener.log` | 리스너 로그 (로컬 런타임) |
15+
| `.claude/data/cache/`, `.claude/data/outbox/` | 사용자 개인 할일 데이터·로컬 캐시 |
16+
| `.claude/skill-invocations.log` | 스킬 호출 로그 (로컬 런타임) |
17+
| `.claude/data/watchdog-state.txt`·`watchdog.log`·`flush-cron.log`·`digest-cron.log` | 시스템 루프(감시·flush·집계) 런타임 상태·로그 |
18+
19+
이 목록은 `.gitignore`와 일치해야 한다. 새 비밀값 파일을 추가하면 **양쪽을 함께 갱신**한다.
20+
21+
## 불변 규칙
22+
23+
1. **토큰·chat_id 전문을 출력하지 않는다** — 로그, 사용자 응답, 커밋, 에러 메시지 어디에도.
24+
실패 응답에도 노출 금지 (예: `{ "ok": false, "error": "토큰 없음" }` — 값은 싣지 않는다).
25+
2. **자격증명 파일은 읽기 전용으로만 접근** — 위 파일에서 값을 읽어 API를 호출할 뿐,
26+
내용을 다른 파일·로그로 복사하지 않는다.
27+
3. **파일이 없거나 비어 있으면 발송/호출하지 않고 실패를 응답한다.**
28+
4. **개인 할일 데이터(cache/outbox)를 커밋하거나 외부로 내보내지 않는다.**
29+
30+
## 자격증명 사용 예 (telegram)
31+
32+
```bash
33+
# .claude/data/telegram.json 에서 읽어 API 호출 — 값을 로그에 남기지 않는다
34+
curl -s "https://api.telegram.org/bot<bot_token>/sendMessage" \
35+
--data-urlencode "chat_id=<chat_id>" \
36+
--data-urlencode "text=<메시지>" \
37+
-w "\n%{http_code}"
38+
```
39+
40+
> 이 원칙은 [`design-principles.md`](./design-principles.md) §6과 같은 규칙의 상세판이다.

0 commit comments

Comments
 (0)