제품 체인지로그
사용자에게 제품의 변경 사항을 알리는 정기 이메일입니다. 사람처럼 작성되고, 안전하게 발송되며, 소셜 게시물로 재활용됩니다. 이메일은 각 이슈의 근간이 되며, 다른 모든 채널은 이메일을 기반으로 축약됩니다.
이 스킬 실행 방식
- 처음에 한 번 질문합니다. 첫 실행 시(또는 설정이 없거나 필요한 값이 비어 있을 때) 설정 질문을 한 묶음으로 간단히 묻고
config.yaml을 작성한 후, 추가 질문 없이 계속 진행합니다. - 그런 다음 모든 것을 처리합니다.
autonomy.mode: autonomous(기본값)에서는 항목 선정, 초안 작성, 린트, 셀프 리뷰, 스크린샷, 테스트 발송, 테스트 이메일 다시 읽기, 대상자 구축, 드라이 런, 전체 발송, 확인, 옵트아웃 수집, 소셜 게시물 작성 및 게시, 보관까지 모두 자체적으로 수행합니다. 승인 단계는 없습니다. 각 외부 작업은 먼저 셀프 리뷰 게이트를 통과해야 합니다. - 비용이 드는 작업만 승인이 필요합니다. 비용이 드는 작업은 모든 모드에서 사용자의 명시적인 동의를 기다립니다: 제공자 플랜의 포함 할당량(
provider.monthly_quota)을 초과하는 발송, 제공자 요금제 업그레이드, 도메인 구매, 유료 도구 또는 라이선스(예: 유료 Remotion 회사 라이선스). 이 외에는 어떤 것도 동의가 필요하지 않습니다. - 하드 스톱은 승인이 아닙니다. 해당 작업을 중단하고, 안전한 나머지 작업을 완료한 후 보고합니다. 하드 스톱을 참조하세요.
- 마지막에 보고합니다. 하나의 요약: 발송된 제목과 전체 텍스트, 수치(대기열, 발송, 실패, 제외, 구독 취소), 배송 샘플, 테스트 이메일 다시 읽기 결과, 각 게시물과 링크, 건너뛴 이유 등이 포함됩니다.
review모드는 기존의 작업별 승인 게이트를 유지합니다. 리뷰 모드를 참조하세요.
모든 스크립트는 기본적으로 드라이 런입니다. 실제 발송은 명시적인 플래그(--send, --yes-send-to-everyone, --yes-send-broadcast)가 필요합니다. 자율 모드에서는 셀프 리뷰 게이트를 통과한 후에만 직접 이러한 플래그를 전달하세요. 스크립트 외부에서 임시로 메일을 발송하지 마세요. 스크립트는 옵트아웃을 강제하는 유일한 방법입니다.
단계 0: 설정 로드
- 현재 프로젝트 루트에서
config.yaml을 찾고, 없으면 이 스킬의 폴더(또는$CHANGELOG_CONFIG)에서 찾습니다. - 설정 파일이 없으면
config.example.yaml을 프로젝트 루트에config.yaml로 복사하고, 한 메시지로 다음을 질문합니다:- 제품명, 웹사이트, 대시보드 및 문서 URL
- 발신자 이름과 직함, 보낼 주소, 회신 주소
- 테스트 수신자(이름과 주소)
- 제공자: Resend 대상자 ID, API 키를 보관하는 환경 변수, 알고 있다면 플랜에 포함된 월간 이메일 수(
provider.monthly_quota, 선택 사항) - 구독 취소 모드(
broadcast또는own_endpoint) - 이미지 호스팅: 스크린샷이 저장될 위치와 공개 기본 URL
- 선택 사항: 주기, 브랜드 링크 색상, 각 소셜 채널 게시 방식(
social.post_via) 이 중 어떤 것도 추측하지 마세요. 답변을config.yaml에 작성한 후 계속 진행합니다.
- 대문자 플레이스홀더나
yourproduct.example주소로 설정된 값은 누락된 것으로 간주합니다. autonomy.mode(기본값autonomous),voice.notes및voice.banned_phrases를 읽습니다. 음성 설정은 참고 문서의 기본값을 재정의합니다.- 비밀 정보는 설정 파일이나 채팅에 절대 저장하지 않습니다. API 키와 구독 취소 서명 비밀은 환경 변수에 저장됩니다.
이후 이슈에서는 질문하지 않습니다. 유일한 선택적 질문은 "이번에 git 외부에서 배송된 항목(가격, 파트너십, 문서)이 있습니까?"입니다. 사용자가 없으면 건너뛰고 git과 릴리스 노트를 기반으로 작업합니다.
단계 1: 포함할 항목 선택
먼저 references/voice-and-format.md를 읽습니다.
paths.issues_dir에서 마지막 이슈(NNN-YYYY-MM-DD-slug형식의 폴더)를 찾습니다. 가장 최근 것이 다음 이슈의 템플릿입니다.- 해당 날짜 이후의 후보를 수집합니다:
git log --since=<date> --no-merges --oneline, 병합된 풀 리퀘스트, 릴리스 노트, 사용자가 언급한 내용. - 사용자에게 보이는 변경 사항만 유지합니다. 3~5개 항목, 독자에게 유용한 순서대로 정렬합니다. 가장 유용한 변경 사항을 맨 앞에 배치하고, 대부분의 독자가 오늘 사용할 수 있는 항목을 우선시합니다.
- 모든 주장을 실제 제품과 웹사이트에서 확인합니다. 기능, 숫자, 날짜를 절대 지어내지 마세요. 확인할 수 없는 주장은 항목을 삭제합니다.
- 목록과 순서를 이슈의
NOTES.md에 작성하고 계속 진행합니다.
단계 2: 이메일 초안 작성
paths.issues_dir/NNN-YYYY-MM-DD-slug/를 생성하고 다음을 작성합니다:
subject.txt: 혜택 중심, 제품 우선("이제 시트로 보고서를 내보낼 수 있습니다"). 절대 "체인지로그 #N"이 아닙니다.email.html: templates/email.html(또는 마지막 이슈)에서 모든[[...]]마커를 설정과 내용으로 채웁니다.email.txt: 동일한 내용을 일반 텍스트로, URL은 그대로 표시합니다.
{{NAME}}과 {{UNSUB_URL}}은 그대로 플레이스홀더로 유지합니다. 발송 스크립트가 수신자별로 채웁니다.
핵심 규칙(자세한 내용은 참고 문서 참조):
Hi {{NAME}},다음에 설정의 한 줄 소개로 시작하고, 바로 주요 항목으로 들어갑니다.- 각 항목: 무엇이 변경되었는지, 왜 중요한지, 자연스러운 문장으로 작성합니다. 라벨이 있는 글머리 기호를 사용하지 않습니다.
- 짧은 문장, 쉬운 단어, "우리가 만들었습니다"보다는 "이제 여러분은 ~할 수 있습니다"를 사용합니다.
- 문단당 최대 한 번의 굵게 강조를 사용합니다.
- 항목당 최대 한 개의 스크린샷; 주요 항목에는 항상 스크린샷이 있으며, 기능에 링크됩니다.
- 스타일이 최소화된 HTML: 600px 너비, 시스템 글꼴, 인라인 스타일, 버튼이나 배너 없음.
- "발표하게 되어 기쁩니다" 및 금지 목록의 나머지 문구 없음.
voice.no_dashes가 켜져 있으면 em 대시나 en 대시를 사용하지 않음. - 선택적인 캐주얼 P.S. ("BTW, ...").
- 설정에서 가져온 서명. 구독 취소 줄은 마지막 줄로, 작고 흐리게 표시합니다.
단계 3: 스크린샷
references/screenshots.md를 따릅니다: 데모 계정에서 실제 상태를 준비하고(고객 데이터 사용 금지), Playwright로 2배율로 캡처하고, 패널까지 자르고, 파일 이름을 <issue>-<slug>.png로 지정하고, paths.image_base_url에 호스팅한 후, 모든 발송 전에 각 URL이 200을 반환하는지 확인합니다.
단계 4: 린트 및 셀프 리뷰 (게이트 1: 카피)
python3 scripts/lint_email.py ISSUE_DIR --check-links
모든 실패를 수정한 후 다음 체크리스트를 수행합니다. 모든 항목이 통과할 때까지 수정하고 다시 실행합니다.
-
lint_email.py --check-links가 0으로 종료됩니다. - 바쁜 고객의 입장에서 위에서 아래로 읽었을 때: 5초 안에 가장 유용한 변경 사항이 명확하게 드러납니다.
- 모든 항목은 사용자가 오늘 보고 사용할 수 있는 것입니다.
- 모든 주장, 숫자 및 링크가 실제 제품 및 사이트와 일치합니다.
- 보도 자료처럼 읽히지 않습니다. 발신자가 직접 말하는 것처럼 들립니다.
- 제목이 60자 미만, 혜택 중심, "체인지로그 #N" 없음.
- 모든 이미지에 대체 텍스트가 있고, 호스팅된 URL이 200을 반환합니다(또는 단계 7 전에 배포될 예정).
단계 5: 테스트 발송 및 읽기 확인 (게이트 2: 배송된 카피)
python3 scripts/send_test.py ISSUE_DIR --send # test_recipients에만, [TEST] 접두사
python3 scripts/send_test.py ISSUE_DIR --readback # 1분 후
테스트 사본은 test_recipients에게만 전송되며, 이미지가 아직 배포되지 않은 경우 인라인으로 포함됩니다. --readback은 제공자 API에서 각 배송된 메시지를 가져와 마지막 이벤트, 제목, 렌더링된 인사말과 구독 취소 줄을 확인하고, 어떤 플레이스홀더도 남아 있지 않은지 확인합니다. 또한 반환된 HTML을 직접 한 번 읽습니다. 문제가 있으면 수정하고, 다시 린트하고, 다시 테스트합니다.
own_endpoint 모드에서는 테스트 사본의 구독 취소 링크를 열어 옵트아웃이 기록되었는지 확인한 후, 테스트 주소를 다시 구독합니다.
단계 6: 대상자 구축
자세한 내용은 references/sending.md를 참조하세요.
- 사용자의 데이터베이스에서
ISSUE_DIR/audience.jsonl을 새로 만듭니다(한 줄에{"email", "name"}). 이전 목록을 재사용하지 마세요. - 옵트아웃 테이블에서
ISSUE_DIR/unsubs.txt를 만듭니다. 브로드캐스트 모드에서는 먼저python3 scripts/broadcast_unsubs.py --out <issues_dir>/unsubs.txt를 실행하고 복사합니다. - 창업자, 테스트 및 데모 계정은 설정의
exclude에 있으며, 대신 테스트 사본을 받습니다.
단계 7: 전체 발송 (게이트 3: 대상자)
unsubscribe.mode에서 경로를 선택합니다. 먼저 드라이 런을 실행하고 수를 확인합니다.
브로드캐스트 모드 (제공자가 구독 취소를 처리):
python3 scripts/broadcast_sync.py ISSUE_DIR/audience.jsonl # 플랜 확인
python3 scripts/broadcast_sync.py ISSUE_DIR/audience.jsonl --apply # 연락처 업서트
python3 scripts/broadcast_create.py ISSUE_DIR --create # 초안만 작성
python3 scripts/broadcast_create.py --send BROADCAST_ID # 수신자 수 표시, 거부
python3 scripts/broadcast_create.py --send BROADCAST_ID --yes-send-broadcast
자체 엔드포인트 모드 (자체 페이지에 대한 서명된 링크):
python3 scripts/blast.py ISSUE_DIR # 드라이 런 및 수 표시
python3 scripts/blast.py ISSUE_DIR --send --yes-send-to-everyone
대상자 체크리스트, 발송 플래그 전에 모두 통과해야 함:
- 게이트 1과 2가 이 정확한 카피로 통과되었습니다(읽기 확인 이후 변경 사항 없음).
- 옵트아웃 목록이 이번 실행에서 가져오거나 구축되었으며, 제품에 구독 취소가 있었던 적이 있다면 비어 있지 않습니다.
- 드라이 런 수치가 합리적입니다: 대기열은 사용자에서 옵트아웃과 제외를 뺀 것과 유사하고, 마지막 이슈의
NOTES.md대비 갑작스러운 증가가 없습니다. - 브로드캐스트 모드:
broadcast_sync.py가 예상치 못한 오래된 연락처를 보고하지 않았습니다. - 호스팅된 모든 이미지 URL이 200을 반환합니다.
- 할당량:
provider.monthly_quota가 설정된 경우,--sent-this-month N을 전달합니다(이번 달 이슈NOTES.md의 발송 수와 동일한 플랜의 다른 메일 합계). 스크립트가 할당량을 초과할 것이라고 말하면 중단하고 사용자에게 질문하고, 사용자의 명시적인 동의 후에만--yes-over-quota를 전달합니다.
개별 발송은 재개 가능합니다: 중단되면 동일한 명령을 다시 실행하면 이미 발송된 주소는 건너뜁니다.
단계 8: 확인 및 동기화
- 개별 발송:
python3 scripts/verify_delivery.py ISSUE_DIR --sample 40. 브로드캐스트:GET /broadcasts/{id}를 제공자 API 또는 대시보드를 통해 확인합니다. - 하루 후 옵트아웃을 다시 가져옵니다(
broadcast_unsubs.py). 사용자가 자체 데이터베이스에 옵트아웃을 유지하고 쓰기 권한이 있으면 가져오고, 그렇지 않으면 보고서에 명시합니다.
단계 9: 소셜 게시물
최종 이메일을 기반으로 references/social.md를 따라 작성합니다:
- X 스레드:
social.hook_prefix로 시작하는 훅 트윗으로 모든 항목을 언급하고, 링크 없이 가장 강력한 스크린샷을 포함합니다. 그런 다음 항목별로 번호가 매겨진 트윗에 링크와 스크린샷을 포함합니다. 창업자를 위한 한 줄 개인 인용문 게시물도 포함합니다. - LinkedIn: 하나의 게시물, 훅 첫 줄, 항목당 하나의 짧은 문단, 일반 텍스트, 이미지 갤러리.
- 커뮤니티(Discord 또는 Slack): 소개 줄, 항목당 하나의 링크가 포함된 한 줄, 게시 후 스레드 링크.
최대 4~5개 항목, 정책 공지 없음, 주장은 실제 사이트와 일치해야 함. social.md에서 동일한 금지 문구 및 대시 검사를 실행합니다(lint_email.py가 읽음). 그런 다음 social.post_via에 도구가 있는 각 채널에 대해 직접 게시하고 게시물 URL을 기록합니다. 도구가 없는 채널의 경우 최종 텍스트와 이미지를 사용자에게 전달합니다. 창업자 인용문 게시물은 개인 계정에서 오므로, 해당 계정에 대한 도구가 구성되지 않은 경우 사용자에게 전달합니다.
단계 10: 비디오 (선택 사항, 요청 시에만)
사용자가 요청하면 references/video.md를 따릅니다: 이슈 스크린샷을 기반으로 한 15~25초 모션 릴, 제목 슬라이드 없음, 항목당 큰 헤드라인, 스크린샷이 지배적, 제품 URL이 포함된 아웃트로. Remotion은 특정 팀 규모 이상에서 유료 회사 라이선스가 필요하며, 이는 사용자의 비용 관련 질문입니다.
단계 11: 보관 및 보고
이슈 폴더에 다음을 보관합니다: subject.txt, email.html, email.txt, 스크린샷, social.md, test_results.jsonl, 그리고 항목 목록, 발송 날짜, 사용된 경로, 대상자 규모, 발송 및 실패 수, 브로드캐스트 ID, 게시물 URL, 하루 후 구독 취소 수가 포함된 NOTES.md. 제품에 공개 웹 체인지로그 페이지(product.changelog_page)가 있고 그 소스가 이 저장소에 있으면, 해당 이슈도 추가합니다. 사용자가 음성이나 형식을 수정하면 설정의 voice.notes에 학습 내용을 추가하여 다음 이슈가 이를 기반으로 시작되도록 합니다. 그런 다음 맨 위에 설명된 하나의 요약을 보냅니다.
하드 스톱
해당 작업을 중단하고, 안전한 나머지 작업을 모두 완료한 후 무엇이 중단되었고 이유가 무엇인지 보고합니다. 절대 스톱을 우회하지 마세요.
- 수정할 수 없는 린트 실패.
- 수정할 수 없는 끊어진 링크 또는 이미지(배포하거나 수정한 후에도 여전히 200 아님).
- 테스트 읽기 확인 실패 및 원인을 찾을 수 없는 경우.
- 옵트아웃 필터링 실패: 옵트아웃 목록을 가져오거나 구축할 수 없음, 대상자 쿼리 오류, 또는 스크립트가
unsubs.txt에 대해 거부. 절대 옵트아웃 없이 발송하지 마세요. - 발송 중 제공자 오류가 임계값을 초과하는 경우(
blast.py가provider.max_failures또는provider.max_failure_rate에서 2로 종료). 오류를 보고하고, 발송은 수정 후 재개 가능합니다. - 할당량을 초과하는 발송, 요금제 업그레이드 또는 기타 비용 관련 작업: 사용자에게 질문(비용 규칙).
- 로그인, 2FA 또는 재인증 프롬프트, CAPTCHA, 플랫폼 경고 또는 게시 도구의 속도 제한 알림, 결제 또는 청구 양식.
- 옵트아웃 또는 안티스팸 법률을 위반할 수 있는 모든 작업(구독 취소 줄 누락, 옵트아웃한 사람을 다시 구독).
- 확인할 수 없는 주장. 추측하지 말고 삭제하세요.
리뷰 모드
autonomy.mode: review에서는 세 게이트 모두 사용자의 확인이 필요합니다:
- 초안 게이트. 채팅에 제목, 이메일 및 항목 목록을 표시합니다. 사용자가 카피를 승인할 때까지 아무것도 발송되지 않습니다.
- 테스트 게이트.
test_recipients에게만 발송합니다. 사용자가 실제 받은편지함에서 읽고 진행하거나 수정 사항을 제시합니다. - 발송 게이트. 드라이 런 수치(대기열, 제외, 구독 취소)를 표시합니다. 사용자가 채팅에서 특정 발송에 대해 동의할 때만 전체 발송합니다. 이전 단계에 대한 동의는 이 단계에 대한 동의가 아닙니다.
소셜 게시물은 게시 전에 표시되며, 항목 목록은 작성 전에 제안됩니다. 해당 게이트를 통과하기 전에 발송 플래그를 절대 전달하지 마세요.
"구독자 수는 몇 명인가요?"에 답변하기
구독자 = 사용자 - 옵트아웃. 사용자의 옵트아웃 테이블 또는 unsubs.txt에서 옵트아웃을 계산합니다(브로드캐스트 모드에서는 먼저 broadcast_unsubs.py 실행). 최신 이슈의 구독 취소율도 제공합니다.
파일
config.example.yaml: 모든 회사별 값과autonomy,provider.monthly_quota,social.post_via.templates/email.html,templates/email.txt: 시작 레이아웃.scripts/:lint_email.py,send_test.py(--readback포함),broadcast_sync.py,broadcast_create.py,blast.py,verify_delivery.py,broadcast_unsubs.py.references/:voice-and-format.md,screenshots.md,sending.md,social.md,video.md.