b3os-github-workflow · diff

git:20260801.9cc5479 to git:20260814.6415eea

37 added, 4 removed. Audit B to B.

---
name: b3os-github-workflow
- description: 팀원이 코드 변경을 GitHub PR로 올려 승인·머지까지 가는 절차. 브랜치·worktree 격리, 커밋 신원(팀원 이름 + 팀 계정), PR 작성 계정, 검증 근거를 PR에 남기기, tier별 승인 요청, 머지 후 실측 확인. 팀원이 git-추적 파일을 고쳐 main에 반영해야 할 때 사용. 승인·안전 정책 자체는 TEAM-OS가 정본이고 여기는 절차만 다룬다.
- trigger: branch·PR·merge
+ description: 팀원이 코드 변경을 GitHub PR로 올려 승인·머지까지 가는 절차. 브랜치·worktree 격리, 커밋 신원(팀원 이름 + 팀 계정), PR 작성 계정, 검증 근거를 PR에 남기기, tier별 승인 요청, 머지 후 실측 확인. 저장소에 남는 글(코드·주석·커밋·PR·이슈·외부 문서)을 사실과 인과만으로 쓰는 규칙도 여기 있다. 팀원이 git-추적 파일을 고치거나 저장소에 남을 글을 쓸 때 사용. 승인·안전 정책 자체는 TEAM-OS가 정본이고 여기는 절차만 다룬다.
+ trigger: branch·PR·merge · writing anything that lands in the repo
---
# GitHub 워크플로 — 변경을 PR로 올려 머지까지
- **PR·문서는 인과관계와 사실로 쓴다. 회고와 리뷰는 저장소 밖에서 한다. 간결하게, 짧은 문장으로.**
+ **코드·주석·커밋·PR·문서는 인과관계와 사실로 쓴다. 회고와 사람의 발언은 저장소 밖에서 다룬다. 간결하게, 짧은 문장으로.**
b3os 인프라(소스·config·registry·릴리스)를 고치면 `b3os-infra-safety` 를 먼저 보고 이 절차를 얹는다.
## 무엇을 쓰나 — 저장소에 남는 글은 사실과 인과만
저장소에 들어가는 것은 전부 공개물이다. 용도가 정해져 있다.
| 어디 | 무엇을 |
|---|---|
| README·가이드 | 하는 방법 + 확인 방법 |
| PR 본문 | 무엇이 문제인가(사실) → 왜 그렇게 되나(인과) → 어떻게 고쳤나 → 검증 결과 |
| 커밋 메시지 | 무엇을 바꿨고 왜 그게 맞는가 |
| 코드 주석 | 이 코드가 무엇을 하는가, 왜 이 조건이 필요한가 |
| **메모리·SHARED.md** | **회고·리뷰는 전부 여기.** 밖에 안 나간다 |
**빼는 것은 회고성 서술이다** — 사고 경위, "오늘 우리가", 작업 과정의 날짜, 그 일에 관계없는 사람 이름.
- 절차가 요구하는 신원은 그대로 쓴다 — 커밋 author, PR 본문의 `Submitted-by`, 코드에서 참조하는 버전·날짜.
+ **이 규칙은 PR·문서만이 아니라 코드와 주석에도 적용된다.** 저장소에 남거나 밖으로 나가는 글 전부가 대상이다 — 코드, 주석, 커밋 메시지, PR, 이슈, 외부 문서.
+
+ ### 빼는 것 셋
+
+ **① 팀 대화 인용.** 팀장·팀원이 무슨 말을 했는지는 저장소에 남기지 않는다. 변경의 근거는 **요구사항이나 측정값**으로 적는다. 누가 요청했는지는 근거가 아니다 — 그게 근거라면 다음 사람은 그 말을 찾을 수 없어 판단을 재현하지 못한다.
+
+ 대상은 **팀 내부 대화**다. **외부 출처는 인용한다** — 업스트림 이슈, 벤더 문서, 표준 문서의 문장이 우회 코드의 유일한 근거인 경우가 있다. 그건 링크와 함께 남긴다. 다음 사람이 실제로 찾아가 확인할 수 있기 때문이다.
+
+ **② 자기비판·반성.** "내가 놓쳤다", "이걸 몰라서 밟았다" 는 저장소 밖으로 간다. 남길 값은 후회가 아니라 **무엇이 참인가**다.
+
+ **③ 사람에게 귀속된 리뷰 이력.** "누구 리뷰에서 나왔다" 대신 **무엇이 관측됐는지**를 적는다.
+
+ ### 이렇게 바꾼다
+
+ | 쓰지 않는다 | 대신 |
+ |---|---|
+ | `팀장님 지시 2026-07-30` | `요구사항:` |
+ | `GD 결정` · `팀 리드 결정` | `제품 결정:` |
+ | `팀장님 실측` · `bill 지적` | `실측:` · `리뷰 지적:` |
+ | `codex 리뷰에서 나온` · `dbak 지적` | `리뷰에서 관측된` |
+ | `오늘 4건이 조용히 사라졌다` | `이 경로에서 4건이 통보 없이 유실된다` |
+ | `내가 세 번째까지는 요청했다` | (삭제) |
+
+ **남기는 것**: 관측값, 재현 조건, 인과, 검증 방법, 그리고 그 판단이 왜 맞는가.
+
+ **절차가 요구하는 신원은 그대로 쓴다** — 커밋 author, `Co-authored-by` 를 비롯한 git trailer, PR 본문의 `Submitted-by`, 리뷰 승인 계정, 코드에서 참조하는 버전·날짜. 이건 인용이 아니라 추적에 필요한 사실이다.
+
+ ★그중 일부는 기계가 읽는 값이다.★ `Approved-by: <이름>` 은 산문이 아니라 머지 게이트가 `merge_approvers_normal` 과 대조하는 입력이다(`skills/b3os-release-ops/scripts/release-preflight.sh`). 이 규칙을 "사람 이름을 빼라" 로 읽고 지우면 게이트가 깨진다.
+
+ ### 왜 이게 규칙인가
+
+ 저장소는 공개물이고, 개인 이름·역할·대화는 코드와 함께 영구히 나간다. 그리고 회고성 서술은 **다음 사람에게 쓸모가 없다** — 그가 필요한 것은 "누가 언제 무엇을 말했다" 가 아니라 "지금 이 조건이 왜 이래야 하는가" 다.
+
+ 회고·리뷰·교훈은 **메모리와 `SHARED.md`** 로 간다. 없애는 게 아니라 자리를 옮기는 것이다.
## 설정에서 읽을 값
아래 값은 **이 문서에 적지 않는다.** 환경마다 다르고, 문서에 박히면 그 자체가 공격 지점 안내가 된다.
| 값 | 어디서 읽나 |
|---|---|
| 팀원 작업용 GitHub 계정 | 설정 `github_team_account` |
| 승인용 GitHub 계정 | 설정 `github_approver_account` |
이 단계를 건너뛰면 PR 이 승인 계정으로 만들어져 머지가 막힌다.
**읽는 법:**
```bash
S=$(curl -fsS http://127.0.0.1:7878/team/api/settings)
TEAM_ACCOUNT=$(printf '%s' "$S" | python3 -c 'import json,sys;print(json.load(sys.stdin)["github_team_account"])')
[ -n "$TEAM_ACCOUNT" ] || { echo "설정이 비어 있다 — 중단"; exit 1; }
```
**비어 있으면 팀장에게 물어서 채운다.** 값을 지어내지 않는다. 물어볼 것은 둘이다 —
**PR 을 올릴 계정**과 **승인할 계정**. 두 계정은 서로 달라야 리뷰 요건이 성립한다.
**쓰는 법:**
```bash
curl -X PUT -H 'Content-Type: application/json' \
-d '{"github_team_account":"<팀 작업 계정>","github_approver_account":"<승인 계정>","merge_approvers_normal":"<승인 가능한 팀원>"}' \
http://127.0.0.1:7878/team/api/settings
# 성공하면 반드시 다시 읽어 확인한다 — 쓰기 성공이 반영을 뜻하지 않는다
curl -fsS http://127.0.0.1:7878/team/api/settings | python3 -m json.tool | grep github
```
**세 값은 한 번에 검증되고 한 번에 저장된다** — 하나가 틀리면 **아무것도 안 바뀐다**
(반쯤 바뀐 보안 설정이 제일 위험하다). 이름에 한글·`@` 를 쓰면 **쓰는 시점에 400** 이다 — 판정기가 못 읽는 값을 넣어두면 **머지할 때 알 수 없는 이유로 막힌다.**
**API 응답에 그 키가 없으면 아직 이 변경이 배포되기 전이다.** 그때 한해 DB 에서 직접 읽어 진행할 수 있다
(그 기능을 처음 올리는 부트스트랩 상황).
**배포 후에는 API 만 쓴다** — DB 직접 읽기는 정상 경로가 아니다.
| 커밋 신원 이메일 | 설정 `github_team_commit_email` |
| tier별 승인자 | 설정 (`merge_approvers_*`) · 판정은 `approvals.ts` |
| 저장소·조직 | `git remote` 실측 |
**설정이 비어 있으면 진행하지 않는다.** 기본값으로 때우면 팀장 개인 계정으로 나가고, 그게 이 절차가 막으려는 바로 그 일이다.
## 핵심 원칙 — 절차는 "확인되는 형태"로
이 스킬의 모든 단계에는 **확인 명령**이 붙어 있다. 이유가 있다.
**안내가 시키는 것과 실제로 벌어지는 일은 어긋날 수 있고, 어긋나도 아무 표시가 없다.**
커밋 메시지에 "테스트 추가"라고 적혀도 리포에 테스트가 없을 수 있다.
그래서 이 절차는 **믿지 않고 확인한다.** 확인 명령이 기대와 다르게 나오면 **그 자리에서 멈춘다**(fail-closed). 조용히 진행하는 것보다 시끄럽게 멈추는 게 항상 낫다.
## 절차
### ① 격리 — 브랜치와 worktree
한 과제 = 한 브랜치. 공유 main 워킹트리에 직접 쓰지 않는다. 상세는 `b3os-infra-safety` ①②.
```bash
git worktree add <경로> -b <브랜치> origin/main
```
### ② 커밋 신원 — 팀원 이름 + 팀 계정 (★조용히 틀리는 지점★)
커밋에 찍히는 이름은 **누가 push 하느냐와 무관한 설정값**이다. 안 걸어주면 **전역 설정이 그대로 쓰이고, 팀장 개인 이름으로 나간다.** 경고는 없다.
**권장 — 설정하지 말고 커밋마다 명시한다.** 그러면 경합 자체가 없다.
```bash
git -c user.name="<팀원 이름>" -c user.email="$(팀 계정 커밋 이메일 조회)" commit -m "..."
```
`git -c` 는 **그 명령 하나에만** 적용된다. 저장소 설정을 쓰지 않으니 남의 값을 덮지도, 남에게 밀리지도 않는다.
아래 `git config` 방식은 그렇게 못 하는 도구를 쓸 때 쓴다.
**이 값은 worktree 별로 갈리지 않는다.** worktree 는 저장소의 `.git/config` 하나를 같이 쓴다
(`extensions.worktreeConfig` 를 켜지 않는 한). 즉 **다른 팀원이 자기 worktree 에서 설정하면 내 것도 바뀐다.**
실제로 그렇게 두 번 어긋났다 — 그래서 **설정하고 끝내지 말고 커밋 직전에 다시 확인한다.**
확인은 **틀린 것을 잡을 뿐 경합을 막지는 않는다.** 두 사람이 같은 시각에 쓰면 나중에 쓴 값이 남는다.
전역(`--global`)은 건드리지 않는다:
```bash
git config user.name "<팀원 이름>"
git config user.email "$(팀 계정 커밋 이메일 조회)"
```
**확인 — 이걸 통과하지 못하면 커밋하지 않는다:**
```bash
git config user.name # 비어 있거나 내 이름이 아니면 즉시 중단
git config user.email # 팀 계정 주소여야 한다. 팀장 개인 주소면 즉시 중단
```
**이름도 본다.** 이메일만 확인하면 이름이 틀려도 통과한다 —
이메일은 팀 계정 하나로 공통이라 늘 맞고, 갈리는 것은 이름이다.
그리고 위에서 적었듯 그 값은 공용이라 **내가 설정한 뒤에도 남이 덮을 수 있다.**
첫 커밋 후 실제로 찍힌 값을 다시 본다. **둘 다 기대값과 같아야 한다:**
```bash
git log -1 --format='%an <%ae>' # <내 이름> <팀 계정 주소>
```
### ③ 구현과 검증
- 검증은 **baseline 실패 수와 mutation 실패 수를 분리해서** 기록한다. 범위(어떤 테스트를 돌렸는지)도 같이 적는다. 범위를 밝히지 않은 green은 좁혀서 만든 green과 구분되지 않는다.
- 수정이 **실제로 고정되었는지**는 되돌려서 확인한다. 고친 부분을 원복했을 때 테스트가 실패해야 진짜 고정이다. 실패하지 않으면 그 수정은 아무도 지켜주지 않는다.
- 새 테스트를 추가했다면 **그 파일이 정말 커밋에 들어갔는지** 본다. 무시 규칙에 걸려 조용히 빠질 수 있다.
```bash
git show --stat HEAD # 의도한 파일이 전부 있는지
```
### ④ push
push 인증이 어느 계정으로 나가든 **승인 게이트에는 영향이 없다.** 게이트가 보는 것은 **PR 작성자**다(⑤).
### ⑤ PR 작성 — 팀 계정으로
PR 작성자가 승인자와 **달라야** 리뷰 요건이 성립한다. 그래서 PR은 팀원 작업용 계정으로 만든다. 계정이 갈리면 **승인이 정상적으로 성립해서 게이트를 우회할 필요가 없어진다**(실측 확인).
**★한 줄로 쓰지 않는다.★** 토큰 조회를 그대로 환경변수에 밀어넣는 형태는 **조용히 실패한다** — 조회가 실패하거나 빈 값을 뱉으면 `gh` 는 빈 변수를 **무시하고 기본 계정으로 나가며, 에러도 내지 않는다**(실측: 빈 값·조회 실패 모두 기본 계정으로 작성됨). 이 스킬의 목적이 기록 정확화인데, 실패하면 정확히 그 목적이 조용히 무너진다.
**조회 → 검사 → 사용을 분리한다:**
```bash
TEAM_ACCOUNT="$(설정 조회)"
[ -n "$TEAM_ACCOUNT" ] || { echo "팀 계정 설정이 비어 있다 — 중단"; exit 1; }
TOKEN="$(gh auth token --user "$TEAM_ACCOUNT")" || { echo "토큰 조회 실패 — 중단"; exit 1; }
[ -n "$TOKEN" ] || { echo "토큰이 비어 있다 — 중단"; exit 1; }
GH_TOKEN="$TOKEN" gh pr create --base main --fill
```
**확인 — 작성자가 팀 계정으로 찍혔는지. 다르면 되돌린다:**
```bash
AUTHOR="$(gh pr view <번호> --json author --jq .author.login)"
[ "$AUTHOR" = "$TEAM_ACCOUNT" ] || echo "작성자가 $AUTHOR 다 — 잘못된 계정으로 생성됐다"
```
작성자가 다르면 **그 PR을 닫고 다시 만든다.** 이미 생성된 PR의 작성자는 바꿀 수 없고, 그대로 두면 기록이 틀린 채 남는다. 위 검사를 "참고"로 흘리지 않는다 — 틀렸으면 되돌리는 것까지가 이 단계다.
> ⚠️ 토큰을 화면에 출력하거나 파일·로그에 남기지 않는다. 변수에 담아 그 변수만 넘긴다.
>
> ⚠️ `GH_TOKEN` 은 `gh` 명령에만 적용된다. **일반 `git push` 는 이 변수를 쓰지 않는다** — push 계정을 바꿀 목적으로 이 변수를 쓰면 조용히 다른 계정으로 나간다. push 계정은 승인 게이트에 영향이 없으니(④) 바꾸려 하지 않는 게 맞다.
### ⑤-b PR 본문 — ★핵심 몇 줄을 맨 앞에★
**근거를 촘촘히 쓴 것과 읽고 판단할 수 있게 쓴 것은 다르다.** 배경 → 원인 → 근거 → 결론 순으로 쓰면
읽는 사람이 **결론까지 스크롤해야 한다.** 상세를 지우라는 게 아니라 **순서를 바꾸라는 것이다.**
```markdown
## 핵심 3줄
1. 무엇이 문제인가 (한 문장)
2. 언제 터지나 / 누가 영향받나
3. 어떻게 했나 + 규모(줄 수)
## 무엇을 바꿨나
| 문제 | 고친 방법 |
|---|---|
---
(이하 상세: 실측 근거 · 왜 이 크기인가 · 검증 · 뮤턴트)
Submitted-by: <내 이름>
```
핵심 줄에는 **판단에 필요한 것만** 넣는다:
| 넣는다 | 안 넣는다 |
|---|---|
| **언제 터지나**(발생 빈도·실제 발생 여부) | 조사 과정·시도했다 만 것 |
| **누가 영향받나**(범위·라이브인가) | 코드 세부 구현 |
| **변경 규모**(실제 코드 줄 수) | 배경 설명 |
**같은 원칙이 적용되는 곳** — 팀장님 보고(결론·판단 요청을 맨 위) · 팀원 리뷰 요청("봐줄 것 N개"를 먼저) ·
커밋 메시지(제목 한 줄이 그 역할).
### 고친 부분만 쓴다
작성 기준은 위 **"무엇을 쓰나"** 절과 같다. PR 본문에서 자주 걸리는 것만 다시 적는다.
| 쓴다 (사실) | 안 쓴다 |
|---|---|
| 무엇이 잘못 동작했나 | "처음엔 ~로 갔다가 되돌렸다" |
| 어떻게 고쳤나 — 방법·판단 근거 | "내가 ~를 안 물어본 게 문제였다" |
| 어떻게 확인했나 — 검증·실측 | 시도했다 만 것·중간에 버린 안 |
| 범위를 그 크기로 정한 근거 | 그 근거에 도달한 과정 |
**무엇이 잘못 동작했는지는 "수정한 이유" 에 자연스럽게 들어간다.** 그건 사실이라 쓴다.
빼는 것은 작업 과정에 대한 회고다.
범위 축소처럼 **결과에 영향을 준 판단은 근거만 남긴다** — "발생 0회로 실측되어 복구 로직은 제외" 는 쓴다. 그 근거에 도달한 과정은 안 쓴다.
### ⑥ 검증 근거를 PR에 남긴다
승인 도장은 "봤다"는 표시일 뿐이고, 실제로 지켜주는 것은 **근거**다. 팀 대화에만 남기면 저장소에는 아무 기록이 없다.
PR 본문이나 코멘트에 이 형태로 남긴다:
```text
검증 범위: <실행한 테스트 범위>
baseline: <실패 수>
mutation: <되돌린 것> → <실패 수>
미검증: <확인하지 않은 범위 — 반드시 적는다>
```
**미검증 항목을 비워두지 않는다.** 전부 확인했다는 주장이 사실인 경우는 드물고, 빈 칸은 "없다"가 아니라 "안 적었다"로 읽힌다.
### ⑦ 승인 요청
tier 판정과 승인자는 **TEAM-OS 정책과 설정을 따른다**(이 문서에서 정하지 않는다).
- tier를 **스스로 낮게 표시하지 않는다.** 애매하면 높은 쪽으로 올린다. 잘못 표시하면 봐야 할 사람을 건너뛰게 되고, 그건 악의가 아니라 판단 착오로도 일어난다.
- 작성자는 자기 PR을 승인하지 않는다. 계정이 갈려 있어도 마찬가지다 — **계정이 아니라 사람(팀원) 기준**이다.
### ⑧ 머지 후 확인
```bash
gh pr view <번호> --json state,mergedAt --jq '"\(.state) \(.mergedAt)"'
git fetch origin && git log origin/main --oneline -1
```
머지됐다고 **라이브에 반영된 것은 아니다.** 실행 중인 프로세스는 이미 메모리에 올린 옛 코드로 계속 돈다. 반영이 필요하면 그건 별도 작업이고 **승인 게이트 대상**이다(TEAM-OS §4).
## ★공개 저장소가 된 뒤 — 달라지는 것★
이 저장소는 공개다. **내 저장소가 아니게 됐다** — 남이 clone·fork 해간 사본이 있다.
### 히스토리를 바꾸지 않는다
**`main` 에 force-push 는 하지 않는다.** 남의 clone 이 깨지고, 이미 퍼진 건 어차피 안 지워진다
(fork 네트워크 · `refs/pull/*` · 미러 · 아카이브에 남는다).
잘못 올라간 것은 **지우지 말고 되돌린다**:
```bash
git revert <문제커밋> # 히스토리를 고치지 않고 앞으로 덧붙여 취소
```
### 예외는 하나 — 시크릿이 올라갔을 때
그때도 순서가 있다. **지우는 게 1순위가 아니다.**
| 순서 | 무엇 | 왜 |
|---|---|---|
| 1 | **키를 폐기·재발급** | 이미 복제됐다고 가정한다. 이게 실제 방어다 |
| 2 | GitHub 에 캐시 제거 요청 | force-push 해도 fork·PR 참조에는 남는다 |
| 3 | 재작성 + force-push | `--force-with-lease` 로 (남의 push 를 덮지 않는다) |
| 4 | 사용자 공지 | "이 시점에 히스토리가 바뀌었으니 다시 clone 하세요" |
**force-push 직전에 원격 tip 이 준비 시점과 같은지 확인한다.**
재작성본은 **만든 시점에 고정**된다 — 그 사이 머지가 들어오면 덮어쓴다.
### 태그는 옮기지 않는다
이미 받아간 사람과 내용이 달라진다. 고칠 게 있으면 **새 버전을 낸다.**
### 브랜치별로 다르다
| 대상 | force-push |
|---|---|
| `main` | **금지** (시크릿 유출 예외만) |
| 작업 브랜치 | 자유 — rebase 후 `--force-with-lease` 는 일상 |
### 외부 기여가 들어오기 시작하면
- **fork 에서 온 PR 은 시크릿에 접근하지 못한다** — 그걸 전제로 워크플로를 짠다
- 리뷰는 **코드만이 아니라 워크플로 변경도** 본다(`.github/workflows/` 수정은 실행 권한 변경이다)
- 우리 내부 관행(계정 분리·승인자 표기)을 외부 기여자에게 요구하지 않는다 — **그건 우리 규율이다**
### ★PR 본문 마지막 줄에 올린 사람을 쓴다★
승인과 ★같은 이유★ 다 — PR 작성 계정이 공유라 **GitHub 만 봐서는 누가 올린 건지 알 수 없다.** 작성자 필드에는 팀 계정만 남았다. 승인 쪽에는 규칙이 있었는데 ★작성 쪽에만 없었다.★
본문 **마지막 줄**에 정확히 이 모양으로:
```
Submitted-by: <팀원 ID>
```
**왜 산문이 아니라 이 모양인가** — `Approved-by` 와 같다. 아래 절에 적힌 실측 결함(인용문·부정문·
할일·조사·역할어·기술 용어·이스케이프 개행)이 그대로 재현된다. 사람이 읽는 문장은 본문 위쪽에 쓰고,
**기계가 읽는 한 줄은 정해진 모양으로 맨 끝에** 둔다.
승인까지 붙으면 한 PR 에 두 줄이 남는다 — **누가 올렸고 누가 승인했는지**가 저장소만으로 확인된다:
```
Submitted-by: <팀원 ID>
```
(승인 코멘트에) `Approved-by: <승인자 ID>`
발견자·검증자 크레딧(`발견 = <팀원 ID>(교차검증)`)은 **이것과 별개**다. 그건 사람이 읽는 정보라 본문 안에 자유롭게 쓴다.
### 리뷰 승인은 본문 첫 줄에 이름을 쓴다
우리 승인은 계정이 공유라 **기록만으로는 누가 했는지 구분되지 않는다.** 그래서 승인 본문에
**기계가 읽는 한 줄**을 남긴다 — 마지막 줄에 정확히 이 모양으로:
```
Approved-by: <승인자 ID>
```
**왜 산문이 아니라 이 모양인가**
원래 규칙은 *"본문 첫 줄에 이름을 쓴다"* 였고, 그걸 나중에 파싱하려 하자 결함이 쏟아졌다 — 인용문(`> <이름> 승인합니다`)·부정문(`<이름> 은 승인 안 했지만`)·할일(`- <이름> 승인 필요`)· 조사(`<이름>이`)·역할어(`<역할> 승인 후`)·기술 용어(`direct-to-<역할>` → **<역할> 이 승인한 것으로**)·이스케이프 개행.
**전부 "사람이 쓴 문장에서 기계가 쓸 값을 되찾으려다" 생긴 것이다.**
승인자는 그 순간 자기가 누군지 안다 — **그때 박으면 판정이 정확 일치가 되고 저 결함이 통째로 사라진다.**
이름은 **`merge_approvers_normal` 에 있는 사람**이어야 한다. 사람 설명은 그 위에 자유롭게 쓰면 된다.
> ### ⚠️ 이 줄이 **하지 않는 것** — 반드시 같이 읽어라
>
> **이 줄은 "누가 봤는지"를 기록할 뿐, 신원을 증명하지 않는다.**
> 승인 계정은 공유이므로 **그 계정을 쓰는 사람은 누구나 다른 사람의 이름을 적을 수 있다.**
> 즉 **모호함은 없애지만 위조는 막지 못한다.**
>
> 위조까지 막으려면 **팀원별 GitHub 계정**뿐이고, 그건 별도 결정이다(팀 결정: 지금은 **기록** 목적).
>
> **또 하나 — 여러 명이 봐도 기록에는 한 명만 남는다.**
> 승인 계정이 공유라 같은 계정의 **최종 리뷰 하나**만 유효하다. 한 사람이 승인한 뒤 다른 사람이 또 승인하면
> **기록에는 나중 사람만 남고 앞사람은 사라진다.** 계정이 둘뿐이라 **원리적으로 2명까지만 표현된다** —
> 나중에 *"3명이 봤다"* 의 근거로 쓰면 **틀린다.**
>
> **이 문단을 지우지 마라.** 한계가 안 적히면 다음 사람은 이걸 "인증"으로 읽는다 —
> *주석은 "차단한다"인데 구현은 안 하고, 테스트 이름이 검사보다 많이 주장하는* 형태를 우리는 하루에 여러 번 겪었다.
### 이 게이트는 **자발적이다**
`release-preflight.sh --mode merge` 는 **사람이 돌려야** 걸린다. CI 필수 검사로 강제돼 있지 않다.
**안 돌리면 그냥 안 걸린다.** 강제할지는 별도 결정이다 — 나중에 *"왜 안 막혔지"* 가 나오면 답은 여기다.
### 리뷰 요청 후 push 했으면 알린다
`dismiss_stale_reviews` 가 켜져 있어 **리뷰어의 승인이 조용히 폐기된다.** 통지가 가지 않는다. 요청해놓고 커밋을 더 얹었으면 **한 줄 알린다.**
## 하지 않는 것
- **우회 경로를 문서에 적지 않는다.** "어느 계정이 무엇을 건너뛸 수 있는지", "어떤 동작이 검사를 안 타는지"는 공개 저장소에 남기지 않는다. 계정 이름보다 이쪽이 위험하다 — 이름은 커밋에 이미 남지만, 약점 지도는 새 정보다.
- **정책을 여기 복제하지 않는다.** TEAM-OS를 참조한다.
- **확인 단계를 건너뛰지 않는다.** 건너뛴 확인은 "통과"가 아니라 "모름"이다.
## 한계 — 정직하게
계정을 나눠도 **실질적으로 분리되는 것은 아니다.** 팀원 계정과 승인 계정이 같은 기기에서 같은 팀원들에 의해 쓰인다. 이 절차가 주는 것은 **누가 무엇을 했는지가 저장소에 정확히 남는 것**이고, 우회 방지는 규율에 의존한다. 이 문서를 "분리됐다"로 읽으면 안 된다.
## 관련
- `TEAM-OS.md` §4(승인·검증 게이트) · §12(동시 작업)
- `skills/b3os-infra-safety/SKILL.md` — b3os 자체를 고칠 때
- `skills/b3os-bwf/SKILL.md` — 과제 워크플로 전체