gadak
midagedev/gadak/CLAUDE.md
현재 유효한 규칙만 담는다. 히스토리·결정 경위는 docs/decisions/와 CHANGELOG의 몫. 여기 없는 도메인 지식은 docs/project/STATEOFPLAY.md(현황· hard-won 목록)와 AGENTS.md(기여 계약)·docs/MIRROR.md(스키마·SQL 쿡북)가 원본이다. - 미러는 버려도 되는 캐시. 원본은 항상 Jira — 그 Jira가 Atlassian Cloud든, gadak이 함께 들고 다니는 아주 미니멀한 셀프호스트 Jira (issuetap, gadak origin)든. 어느 쪽이든 미러는 origin에서 다시 만들 수 있고, gadak 자신은 원본을 보관하지 않는다. gadak에만 존재하는 원본 데이터를 만드는 변경은 금지 — 예외는 local.db(방문·검색 기록)와 저장된 뷰이고, 그것들은 export 가능해야 한다. - 영속은 origin의 몫이다. gadak origin…
CLAUDE.md43 starsChanged 37 days ago
- Installs packages
# gadak — 세션·에이전트 공통 계약
현재 유효한 규칙만 담는다. 히스토리·결정 경위는 `docs/decisions/`와
CHANGELOG의 몫. 여기 없는 도메인 지식은 `docs/project/STATE_OF_PLAY.md`(현황·
hard-won 목록)와 `AGENTS.md`(기여 계약)·`docs/MIRROR.md`(스키마·SQL 쿡북)가 원본이다.
## 제품 불변 조건 (깨면 제품이 아니다)
- **미러는 버려도 되는 캐시.** 원본은 항상 Jira — 그 Jira가 Atlassian
Cloud든, gadak이 함께 들고 다니는 아주 미니멀한 셀프호스트 Jira
(`issuetap`, gadak origin)든. 어느 쪽이든 미러는 origin에서 다시
만들 수 있고, **gadak 자신은 원본을 보관하지 않는다.** gadak에만 존재하는
원본 데이터를 만드는 변경은 금지 — 예외는 `local.db`(방문·검색 기록)와
저장된 뷰이고, 그것들은 export 가능해야 한다.
- **영속은 origin의 몫이다.** gadak origin 의 영속 공간은
issuetap의 persist 파일이며 미러가 아니다. 그래서 백업 대상은 `gadak.db`가
아니라 그 파일이고, **워크스페이스는 origin 하나에 묶인다** — origin을
바꾸는 것은 설정 편집이 아니라 새 워크스페이스다. 이 조항을 어기는 것이
"다른 트래커를 조용히 가리키게 하는" 부류의 결함이다.
- **쓰기는 전부 origin(Jira)을 통과**한 뒤 미러 갱신. 미러에 직접 쓰는 API를
열지 않는다. 위키도 같은 규칙이다 — 페이지 쓰기(생성·편집·코멘트,
GDK-380/381/382)는 origin.Wiki를 통과한다: connected는 Confluence REST,
gadak origin 은 issuetap 의 Confluence API(미러 직접 쓰기가 아니다).
- **아웃바운드 없음.** 텔레메트리 금지. 나가는 요청은 사용자가 설정한
origin(Atlassian 사이트·Linear: api.linear.app/uploads.linear.app), 페어링한
home serve, 사용자가 직접 실행한 `gh`·라이브러리 다운로드, loopback뿐
(업데이트 체크는 GDK-1626으로 제거됐다 — 부재를 카피로 되살리지 않는다)
(`SECURITY.md`가 정본).
- **계정·서버·포트 강제 없음.** loopback 단일 사용자 모델 (`docs/decisions/0003`).
## 스키마·계약
- 0.x에서 약속된 것은 **셋뿐**: `issues_full`+RECIPES 쿼리 / `gadak sql`
stdout 형식 / `views open --keys -` 의미. 원본:
`specs/000-product/data-model.md` 상단. 문서에서 "스키마 전체가 계약"
이라고 쓰지 마라.
- **상태·우선순위·이슈 유형은 display name으로 키하지 않는다.**
`status = 'In Progress'`는 한국어 계정에서 소리 없이 0행이다. 항상
`status_category` (new|inprogress|done) 또는 `status_id`; 우선순위는
`priority_rank` (또는 `priority_id`); 유형은 `issue_type_id`. 이 함정은
코드·문서·툴 설명 어디서든 재발 금지.
- **클라이언트가 카탈로그에서 고른 정체를 서버가 다시 해석하지 않는다** (GDK-1982,
2026-09-17). 웹·폰이 `GET {key}/transitions/` 가 준 객체의 id 를 돌려보내면 그것은
기계가 이미 확정한 정체이지 사람이 친 말이 아니다 — 그 id 를 사람 어휘 리졸버
(전이 id → 상태 id → 이름 → 카테고리)에 다시 태우면, 마이그레이션된 워크플로에서
전이 id 와 상태 id 가 겹치는 순간 리졸버가 자기 자신을 모호하다며 거절한다
(빌트인 트래커의 Done 이 어떤 버튼으로도 안 눌리던 그 결함). 기계 경로는 목록과
**정확 일치**로만 답하고 미스는 제시된 id 를 이름으로 부르는 거절이다. 선례는
`internal/create/resolve.go` 의 `matchType` — 단계가 배타적이라 정확한 id 가 맞으면
이름 단계로 흘러가지 않는다. 사람 어휘는 CLI 에 남는다. 같은 클래스의 잠재 위험이
링크 타입에 하나 남아 있다(GDK-1983: 드롭다운이 타입 id 가 아니라 표시 문구를 보낸다 —
문구가 방향까지 나르기 때문인데, 두 타입이 같은 문구를 쓰면 UI 가 제시한 선택지를
서버가 거절한다).
- `time-in-status`는 저장 컬럼이 아니다 — `status_changed_at`에서 계산.
(`data-model.md`가 "deliberately absent"로 명시.)
- decisions/ 문서는 **개정하지 않는다. Addendum만 추가.**
- **CHANGELOG는 릴리스마다 주제 두세 개다 — 사건의 나열이 아니다** (사용자 지시
2026-09-09 "개발과정에서 사고치고 수습하고 단계별로 수정하고가 아니라 이전 버전
대비 달라진 핵심이 잡혀야 하는데 자꾸 사건들의 나열이 된다"). 한 릴리스 절은
**굵은 머리로 시작하는 주제 문단 최대 셋**이고, 각 문단은 직전 버전 대비 무엇이
달라졌는지를 말하며 그 주제에 속하는 `GDK-nnn`을 문장 안에 인용한다. 굵은
머리 없는 문단은 바로 위 주제의 연속이다. **최상위 불릿(`- `)은 금지** — 그게
사고 일지의 모양이다. 재는 것은 `tools/doc-checks.sh` #46(세 판본 전부).
Unreleased에 항목을 더할 때도 새 불릿을 붙이지 말고 있는 주제 문단에 문장을
넣거나, 셋째 주제가 정말 새로 생겼을 때만 문단을 연다.
- **CHANGELOG는 다시 쓸 수 있다 — 서사를 위해서라면 과감하게 정리한다**
(사용자 지시 2026-09-09). 항목을 합치고, 순서를 바꾸고, 줄이고, 키 없는 항목은
지워도 된다. 불변은 셋이다: ① **릴리스 헤딩과 그 순서**(사이트가 그것으로
점프 목록을 만들고 doc-checks #27이 세 판본을 대조한다) ② **살아남은 항목의
숫자·명령·경로·플래그·코드·링크** ③ 없던 일을 새로 쓰지 않는다.
**키는 불변이 아니다** (사용자 결정 2026-09-13): 독자가 못 느끼는 항목은
`GDK-nnn`째로 지운다 — 0.22.0 이 그렇게 345키 4,983단어에서 19키 479단어가
됐고, 감사·리팩토링 라운드 문단이 통째로 빠졌다. 판단은 "이걸 gadak 여는
사람이 알아채는가" 하나이고, 지운 키의 참조 정의도 같이 빠진다
(`python3 tools/changelog-tails.py --write <파일>`). 재는 것은
`bash tools/changelog-preserve.sh <파일> [ref]` — 헤딩이 움직이면 진짜
실패이고, **키·숫자·코드 스팬 축은 항목을 지운 라운드에서 정당하게 빨갛다**
(그 출력을 읽고 무엇을 왜 지웠는지 커밋 메시지에 적는다). 판본은 셋이고(`CHANGELOG.md`·`.ko.md`·`.ja.md`) 여기서는
병렬 판본이 아니라 같은 역사의 번역이다 — **영문 구조를 바꾸면 ko·ja도 같은
구조로 따라가야 한다**(doc-checks #27이 절별 키 집합 일치를, #46이 주제 수를
잰다). `GDK-nnn`을 공개 백로그(`…/gadak/backlog/#/?ks=<KEY>`)로 참조 스타일
링크로 잇는 것은 여전히 기본값이다.
- **Go 소스 게이트는 `bash tools/sourcelint.sh`다 — `go test ./...`가 아니다**
(2026-09-11, 런 34524504178). 소유자 표·정본 위치를 재는 트리 전체 파싱
게이트(`canonical_gate_test`·`stale_gate_test` 류)는 `sourcelint` 빌드
태그 뒤에 있어 태그 없는 `go test`는 그것을 아예 컴파일하지 않는다. CI의
Go-tests 스텝은 그 스크립트를 돌린다. 파일을 옮기거나 쪼갠 커밋(agent.go
5분할)이 로컬 전부 초록인 채 CI만 빨갰다 — 소유자 표가 옛 경로를 들고
있었다. Go를 건드렸으면 `go test ./...` 자리에 이 스크립트를 쓴다.
- Go: `go build ./...` · `go test ./... -count=1` · `go vet ./...` ·
**`gofmt -l` 빈 출력**(CI에 gofmt 잡이 있다 — 2026-08-24 tokencheck 정렬로
로컬 전부 초록인데 CI만 빨간 사고. 신규 .go 파일이 있는 커밋은 필수)
- **루트 Go 코드에 서드파티 import가 새로 생기면 `desktop/`(별도 go.mod)도
게이트다**: `cd desktop && go mod tidy && go build ./...`. 2026-08-23
GDK-635에서 internal/ 신규 패키지의 runewidth import가 desktop go.sum에
없어 데스크톱 CI 3개 잡이 빨갛게 됐다 — 로컬 go 전체는 초록이었다.
- **Go 정적 분석은 `bash tools/staticcheck.sh`이고 반드시 GOOS 매트릭스다**
(GDK-1463). darwin 단독 실행은 `//go:build` 반대편에서만 호출되는 함수를
U1000 죽은 코드로 오탐한다(`parseProcStartTime`·`protocolDefaultIcon`
실측). 세 GOOS가 모두 동의한 것만 실패이고 나머지는 informational로
인쇄된다. ST1005는 제외(한국어 에러 문장·의도된 다중행 프로토콜 에코).
CI 잡 `Staticcheck`은 게이트다(2026-09-09, 잔여 14건을 0으로 만든 뒤
`--warn-only`를 뗐다) — cross-platform 발견 하나가 CI를 빨갛게 만든다.
- **OS별 경로·카탈로그를 만드는 Go 코드는 `goos`를 인자로 받고 세 GOOS를
테스트로 잰다** (2026-09-08, 런 34233540027). `integrations.listFor(goos)`
가 goos 카탈로그를 약속하는데 Claude Desktop 행만 런타임 바인딩 helper를
불러 **호스트의 경로**를 답했다 — darwin 에서는 요청과 답이 우연히 같아서
로컬 go 전체가 초록이고, Linux CI 에서만 `~/.config/Claude` 로 죽는다.
`…For(goos, …)` 씨임을 만들어 놓고 정작 그 행이 안 쓰는 것이 실제 형태였다.
테스트는 한 GOOS 만 재지 말고 darwin·linux·windows 셋을 같은 호스트에서
전부 재고, 실패 메시지에 **어느 goos 를 물었는지** 적는다(그게 없으면 CI
실패가 경로 형식 문제처럼 읽힌다).
- 웹: `make typecheck` (svelte-check). e2e: Playwright, CI 세트는
`e2e/*.spec.ts`(demo/·hosted/·perf/ 제외 — `e2e/playwright.config.ts`).
- **`mobile/`은 루트 게이트가 보지 않는다** — 자기 tsconfig·자기 lockfile을
갖고 있으면서 `web/src`의 일부 모듈(i18n 카탈로그, `lib/terminal/protocol`)을
직접 import한다. **그 둘 중 하나를 건드렸으면** `cd mobile && npm test &&
npm run check && npm run lint:ios`도 게이트다 (2026-08-26: 웹에서 지운
i18n 키를 `mobile/src/screens/Shell.svelte`가 계속 써서, 로컬 go·typecheck·
vitest·e2e 326·doc-checks가 전부 초록인 채로 Mobile 잡만 빨갰다).
**`mobile/src`의 화면·스토어를 건드렸으면 `npm run viewport-gate`까지가
게이트다** — `mobile/e2e/`(Playwright, 402×874 + 셸 6종)는 위 세 명령이
전혀 보지 않는데 CI Mobile 잡은 돌린다 (2026-08-29: dev 셸 자동 채택이
"페어링 없으면 탭 3개" 계약을 깼고, 로컬 go·web e2e 378·mobile vitest·
check·lint:ios가 전부 초록인 채로 CI에서 8개가 빨갰다). 반대 방향도 있다:
**폰에서 i18n 키의 마지막 사용처를 지우면 웹 `npm run test:unit`이
게이트다** — `web/src/lib/i18n/catalog.test.ts`가 미사용 키를 빨강으로
만든다 (2026-09-07 GDK-1542: mobile만 건드린 커밋이 CI Frontend unit만
적색, 로컬 mobile 게이트 넷은 초록). GDK-1540부터 이
게이트는 dev 서버가 아니라 **빌드된 번들**(`vite preview`)을 찍는다 — 다른
라운드의 편집이 스펙 중간에 리로드를 일으키지 않고, 포트는
`GADAK_MOBILE_E2E_PORT`/`GADAK_MOBILE_API_PORT`(기본 5182/7899)로 라운드마다
달리 줄 수 있으며, 다른 워크트리가 띄운 서버는 스탬프 대조가 거절한다.
약 1분(빌드 포함) — 병렬 라운드에는 스펙에 포트 쌍을 명시 배정.
- **브랜드 마크(`docs/media/logo.png`)를 건드렸으면 `make brand`가 게이트다.**
데스크톱은 빌드 때 그 로고를 리사이즈하니 새 마크를 자동으로 집지만, 폰
아이콘은 생성해 커밋하는 파일이라 따라오지 않는다 — 2026-08-27까지 폰은
스캐폴드 기본 아이콘을 달고 있었다. 재생성 없이 커밋하면
`tools/check-brand-icons.sh`(CI Mobile 잡)가 빨강이다. iOS 세트는 트리에
사본이 둘(`mobile/src-tauri/icons/ios/`와
`mobile/src-tauri/gen/apple/Assets.xcassets/`)이고 `tauri icon`은 후자만
쓴다 — `tools/brand/mobile-icons.sh`가 앞쪽을 미러링하고, 게이트가 둘의
일치를 잰다.
- **e2e 직렬화의 실체는 락 파일이 아니라 포트다** — 그리고 포트의 단일
소유자는 `GADAK_E2E_PORT`(기본 7877, `e2e/helpers.ts` `e2eServePort()`)다.
홈은 포트별로 격리된다(`e2e/.tmp/home-<port>`) — GDK-672 랜딩으로 **서로
다른 포트를 준 스위트 두 개는 병렬 가능**하다(병렬 라운드에는 스펙에
포트를 명시 배정 — **포트는 둘 이상 띄어서**: `built-in-attachments.spec`이
자기 serve를 `GADAK_E2E_PORT+1`에 띄우므로 8043·8044 같은 이웃 배정은 그
스펙 넷을 결정적으로 죽인다. 2026-09-11 실측: healthz가 옆 스위트의
serve에 200을 받아 데모 픽스처를 열었고, 있지도 않은 회귀를 40분 추적했다.
구조적 봉쇄는 GDK-1789). 같은 포트 위의 충돌·낡은 서버 재사용은 여전히 경쟁
신호이고(스탬프 불일치는 `assertServedArtifact`가 잡는다), 한 스위트
안은 `workers: 1`이다. **포트를 배정하기 전에 `lsof -nP -iTCP:<p> -sTCP:LISTEN`으로
비었는지 확인한다** — 다른 세션의 프로세스가 물고 있으면 webServer가 exit 1로
죽어 스위트가 실행도 못 한다. **중단(TaskStop)한 Playwright 런의 포트는
버린다**: 부모만 죽고 `e2e/.tmp/gadak-<port> serve` 자식은 남아 있어, 같은
포트로 다시 띄우면 낡은 serve에 붙어 수백 건이 수백 ms 안에 연쇄 실패한다.
- **터미널 e2e(`e2e/terminal*.spec.ts`·`e2e/issue-command.spec.ts`)를
건드렸으면 `npm run test:e2e:wide-prompt`도 게이트다.** 그 스위트는 pane의
셸을 `e2e/ci-shell.sh`로 바꿔 리눅스 CI 러너의 환경 셋을 재현한다 —
24열 프롬프트(로컬 macOS는 훨씬 짧아 줄이 안 접힌다), Ubuntu 기본
`.bashrc`의 창 제목 OSC(종결자가 BEL이다), 그리고 **배너 없음**(macOS
bash 3.2의 zsh 안내 3줄이 모든 출력을 세 행 아래로 밀어 결함 하나를
가리고 있었다). 2026-08-30: 새 터미널 e2e 5건이
로컬 397 전부 초록인 채 CI에서만 죽었고, 원인은 접힌 줄을 읽는 방식·BEL
판정·xterm 링크 캐시 셋이었다. 버퍼를 읽을 때는 스펙마다 `readTerm`을 다시
쓰지 말고 `e2e/helpers.ts`의 것을 쓴다(접힘의 단일 소유자).
- **도크를 연 스펙이 `ControlOrMeta+<키>`를 누르면 Linux 에서만 죽는다**
(2026-09-12, 런 34664828627). 살아 있는 pane 은 키보드를 가져간다
(`onAttached` 가 `renderer.focus()`) 그리고 VT 는 Backquote 계열을 뺀 모든
Ctrl 코드를 자기 것으로 쓴다(`isAppChord`) — Ctrl+K 는 셸의 kill-line 이니
그게 맞다. 함정은 철자다: `ControlOrMeta` 는 macOS 에서 Meta(통과)이고
러너에서 Control(xterm 이 먹음)이라, 작성한 기계에서만 초록이다. 실측:
도크에 포커스를 준 채 Control+k → 팔레트 행 0, Meta+k → 1. 나가는 길은
제품이 이미 가진 코드다 — `Ctrl+Shift+\``(`terminal-focus-strip`, "닫지 않고
VT 를 떠난다"), 단 pane 이 `data-attached` 를 말한 **뒤에** 눌러야 한다
(`onAttached` 가 그 전의 탈출을 되돌린다). 재는 것은
`e2e/terminal-chord-escape.unit.ts`.
- 게이트 단언 완화는 ①귀속 주석 ②정당한 파생 ③FAIL-first 증거 셋 모두
있을 때만.
- **`web/src/app.css` 의 `@theme` 토큰(색·모션·간격)을 건드렸으면 `npm run theme-check`
와 `node tools/token-catalog.mjs`(catalog.json 재생성·커밋)도 게이트다** (2026-09-02:
bg-hover 를 잉크 토큰으로 바꾼 뒤 로컬 go·typecheck·vitest·Playwright 432 전부
초록인 채로 CI 만 두 번 연속 빨갔다 — 첫 번째는 hex 전제, 두 번째는 카탈로그
불일치. 둘 다 CI 의 Theme check 잡만 본다).
- 문서 사실성 가드: `tools/doc-checks.sh` (있으면 커밋 전 실행).
- **셸 도구에서 `sort | head` 는 Linux 에서만 stderr 를 낸다** (2026-09-10, 런
34442778376). GNU sort 는 `head` 가 파이프를 먼저 닫으면 "write failed: Broken
pipe" 를 stderr 에 쓰고 BSD sort 는 조용하다 — 로컬 전부 초록, CI 만 빨강인
부류. 앞 N 줄은 `awk 'NR<=N'` 로 자른다(입력을 끝까지 읽는다).
`tools/audit-test.sh` 가 `tools/audit/*.sh` 에서 그 형태를 잡는다.
- **origin 표면을 바꿨으면 `docs/SUPPORT_MATRIX.md`도 같은 커밋이다** (GDK-1300,
2026-09-02). Jira·Linear·Built-in 세 열의 단일 소유자이고 README 둘은 링크만
한다. `internal/origin/writer.go`·`linearwriter.go`·`internal/linear/`·
`internal/sync/`·issuetap 의존성 범프·CLI 동사의 origin 분기(`gadak open`
같은)를 건드린 커밋은 해당 셀과 `[^n]` 각주의 `path:line`을 갱신한다.
`tools/doc-checks.sh` #39 는 구조(파일·헤더·셀 형식·각주 정의·README 링크)만
잡고 셀의 참·거짓은 리뷰의 몫이다 — 코드에서 생성하는 것은 GDK-1301.
- **어휘 일괄 치환은 계약 문자열을 삼킨다 — 치환 뒤에 계약을 따로 세어라**
(2026-09-02 GDK-1278 어휘 리네임, 5회 발생). 저장 값·에러 코드·요청 필드·
라우트·데이터 id 접두사·DOM testid 는 어휘가 아니라 wire 계약인데, `\bword\b`
치환은 주석과 구분하지 못한다. 실측으로 삼킨 것: 플래그 이름
(`--replace-standalone` → 없는 플래그), 미러 데이터 id
(`standalone-jira:`), API 라우트 정규식(`onboarding/standalone/`),
식별자 자리에 들어간 산문(`Local-origin bool`), 그리고 **MCP 툴 서술의
enum**. 앞의 넷은 게이트가 잡았고 다섯째는 아무 게이트도 안 봤다 —
**`internal/mcp/tools.go` 의 서술은 게이트가 없는 표면이다**: 서버가 내지
않는 값을 가르쳐도 go·doc-checks·e2e 전부 초록이고, 그것을 읽는 것은
셸 없는 에이전트뿐이다. 리네임 커밋은 게이트 전부를 뒤에 세운 한 덩어리로
하고, 커밋 전에 `grep` 으로 wire 계약 목록을 눈으로 확인한다.
- **IP 리터럴·호스트명·홈 경로가 들어가는 커밋은 `bash scripts/scan-internal.sh`도
게이트다** — CI의 secret/internal-string 스캔과 같은 스크립트이고, 로컬
게이트 목록에 없어서 두 번 연속 CI만 빨갰다(2026-08-25: 테스트 표의 CGNAT
대역 주소 하나, 그리고 그것을 인용해 이 규칙을 적은 문장 자체). 픽스처의
"원격 주소"는 TEST-NET(`192.0.2.x`/`198.51.100.x`)을 쓰고, 걸린 주소를
문서에 다시 적지 마라 — **스캐너는 편집이 끝난 뒤에 돌린다.**
- **`web/`·`e2e/`·i18n 카탈로그·`examples/demo.db`를 건드렸으면 Playwright는
선택이 아니다.** "영향 게이트만" 판단이 e2e를 건너뛰는 것이 실제 사고
경로였다 (2026-08-16: 온보딩 카피 변경이 e2e 기대값을 낡게 만들었고,
로컬은 go·typecheck·vitest·doc-checks 전부 초록이었다. 2026-08-21:
fixture 재생성이 item_refs를 비웠는데 go 전체·doc-checks가 초록이라
cross-links e2e 빨강을 CI에서야 봤다 — e2e가 읽는 fixture도 e2e의 일부다).
- **푸시는 끝이 아니다 — CI 초록이 끝이다.** 푸시 직후
`tools/ci-status.sh`(HEAD의 런을 기다려 결론을 내고, 빨간 상태 위에
쌓았으면 그것도 알려준다). 라운드 완료 보고에 그 결과를 쓴다.
- **PR은 로컬에서 못 도는 잡에 걸리는 변경만** (사용자 결정 2026-08-19).
리뷰어가 없으므로 PR이 사는 것은 리뷰가 아니라 "CI 평결이 main이 아니라
브랜치에 떨어진다" 하나뿐이다. 그 값이 실제로 나오는 것은 로컬 게이트가
대신할 수 없는 잡뿐이다 — `desktop/`, `.github/workflows/`, 팩 스크립트
(Desktop Windows build는 `windows-latest`, Desktop Linux build는 GTK4·
WebKitGTK·AppImage). **그 외에는 로컬 게이트가 전부 초록이면 main에 직접
푸시**하고 `tools/ci-status.sh`로 확인한다. 기본값은 직접이다.
- **PR 두 개 이상이 동시에 열려 있으면 리베이스는 손으로 하지 않는다** —
`tools/rebase-pr.sh <branch>`. main에 뭘 올릴 때마다 열린 PR 전부가 뒤로
밀리고, 충돌은 매번 같은 두 곳이다: CHANGELOG 참조 링크 꼬리(양쪽이 다
맞으니 둘 다 유지)와 `examples/backlog-snapshot.tar.gz`(바이트는 머지하지
않고 재생성). 그 둘 밖의 충돌은 진짜 충돌이라 스크립트가 exit 2로 멈춘다.
푸시는 스크립트가 하지 않는다.
- 로컬 Node는 CI와 같아야 한다 — 버전의 단일 소유자는 `.nvmrc`(`nvm use`).
로컬 24/CI 20 격차가 결함 하나를 여러 푸시 동안 숨긴 적이 있다(GDK-57).
- **의존성을 하나라도 건드렸으면 `bash tools/check-lockfile-platforms.sh`도
게이트다.** macOS에서 `npm install`이 lockfile을 재해결하면 npm 10은
darwin-arm64 것만 남기고 다른 플랫폼의 optional 네이티브 바이너리를 전부
지운다(2026-08-26: `@rollup/rollup-*` 75→26, `@esbuild/*` 78→27). 로컬은
전부 초록이다 — 없어진 게 이 머신이 안 쓰는 것들이라서. CI는 linux-x64라
`vite build`가 두 잡에서 죽었다. **고칠 때 lockfile을 지우고 재설치하지
마라** — 그게 애초에 잘라먹은 경로다. 플랫폼이 살아 있던 마지막 lockfile을
복원한 뒤(`git show <ref>:package-lock.json > package-lock.json`)
`npm install --package-lock-only`로 바뀐 의존성만 갱신한다.
- 데모 fixture는 `examples/demo.db`(이슈 534). 수치를 문서에 박을 때는
실측 후, 가능하면 숫자 자체를 빼라.
- **미러 스키마 마이그레이션(schemaVNN)을 추가한 커밋은 `make demo-fixture`가
게이트다** — e2e serve가 fixture의 user_version 불일치를 거절해서, go 전체
초록인 채 CI e2e만 빨갛다(2026-08-31: schemaV40이 정확히 이 경로로 074c9dcd
를 적색으로 만들었다 — 로컬에서 스키마 커밋에 Playwright를 건너뛴 것이 원인).
재생성은 항상 make 타깃으로(스크럽 생략 금지), 재생성 뒤 Playwright 전체.
- **정적 픽스처(`examples/demo.db`) 위의 날짜·나이 단언은 픽스처 자기 시각에
고정한다** — `julianday('now')`·`time.Now()`가 아니라 픽스처의
`MAX(updated_at)`(`internal/snapshot/fixture_flow_test.go` `fixtureNow`).
벽시계로 재는 단언은 커밋 없이 날짜만으로 CI를 빨갛게 하는 시간폭탄이다
(GDK-1859 retro 주 경계, GDK-1881 WIP 나이).
## 배포·이름
- 커밋·태그·푸시·릴리스는 **리드 세션 전용**. **main 푸시는 매번 묻지
않는다** (사용자 지시 2026-08-26 "묻지말고 푸시해도 괜찮아") — 게이트가
전부 초록이면 그대로 올리고 `tools/ci-status.sh`로 확인한다. 승인이
여전히 필요한 것은 **태그·릴리스 게시**와 공개 스토어 제출이다.
- **`~/repo/issuetap` 도 같은 권한이다** (사용자 지시 2026-09-09 "이수탭도
편하게 작업해"). gadak origin 을 고치는 일은 gadak 작업의 일부이고, 그
레포의 게이트(`go vet` · `go test ./...` · `scripts/secretscan.sh` ·
건드린 파일의 `gofmt`)가 초록이면 묻지 않고 main 에 올린다. 커밋 신원은
`~/repo` 아래라 midagedev 로 자동이지만, 공개 푸시 전에 한 번 확인한다
(`git log -1 --format='%ae'`). 그 뒤 gadak 의 `go.mod` 핀을 올리는 것은
**별개 커밋**이다 — 핀 범프는 `desktop/go.mod` 까지 게이트다.
- **릴리스가 게시되면 `tools/release-notes.sh <tag> --apply`까지가 게시다.**
goreleaser는 커밋 로그와 설치 푸터만 올린다 — 사람이 읽을 요약(그 태그의
CHANGELOG 절 + 참조 정의)은 이 스크립트가 생성 로그 위에 얹는다. 손으로 하던
단계라 v0.20.0·v0.21.0엔 있고 v0.22.0엔 없었다(2026-09-13). **태그 뒤에
CHANGELOG 절을 고쳤으면 다시 돌린다** — 멱등이고, 사이트 `/changelog/`는
main에서 자동으로 따라가지만 릴리스 페이지는 태그 시점 본문에 멈춰 있다.
릴리스 페이지 편집은 공개 콘텐츠라 사용자 승인 뒤에 `--apply`.
- brew: `gadak` = **macOS 앱 cask**(CLI 포함, v0.14부터 tap에 게시),
`gadak-cli` = CLI formula(리눅스 포함). 문서의 설치 명령은 태그와 동시
교체.
- **폰 앱(mobile/)의 TestFlight 내부 배포는 한 줄이다**:
`cd mobile && scripts/testflight-upload.sh --bump` — 게이트 → `tauri ios
build` → **`.ipa` 계약 검증 9항목** → `altool` 업로드 → 처리 대기 →
`artifacts/app-store/`에 영수증. 계정 소유자 웹 단계(그룹 배정·계약 갱신
동의)와 rust 툴체인 함정은 `docs/runbooks/testflight-release.md`가 정본.
자격증명은 `~/.appstoreconnect`(레포 밖). 공개 스토어 제출은 여전히 별개
판단이고 리뷰어 데모 경로가 선행 조건이다(GDK-805).
- 에이전트 온보딩은 **skill-first**: 셸 있는 호스트는 `gadak skill install
[client]` — claude(기본)·codex·agents(`~/.agents/skills`, agentskills 계열
전부)·cursor·gemini·opencode·grok, 전부 같은 SKILL.md가 호스트별 경로에
들어간다(GDK-1508; Codex는 그 파일을 그대로 읽는 것을 실측). MCP(`gadak
mcp install <client>`)는 셸 없는 호스트(Claude Desktop)용.
- **리드 세션도 skill-first다**: gadak CLI를 만지기 전에 `Skill(gadak)`을
로드한다 — 동사 추측 금지 (2026-08-27 실측: 스킬이 current로 설치돼
있는데 리드가 안 읽고 view→show→get을 연속 추측, 셋 다 실패. 정답은
`gadak issue`. 에러 개선은 GDK-1015).
- `make media`는 `media-mcp`를 포함하지 않는다 — mcp 클립은 Claude 로그인과
실모델 호출이 필요해서 기여자에게 강제하지 않는다 (`docs/project/MEDIA.md`).
- **업스트림 PR은 `docs/runbooks/upstream-pr.md`의 파이프라인을 통과한 뒤에만 제출한다**
— 사전조사(중복·분류·머지 선례) → 실측 FAIL-first(코드 리딩만으로 확정한
결함은 가설이다) → 형제 구현 패리티 패치 → 제출 전 적대적 리뷰(예상 지적을
고치거나 본문 한 문장으로 선제) → 정직한 검증 경계. 근거 사례: wails#6000
(봇 지적 1건, 답변 1회로 철회) / dock-reopen (실측이 비버그를 제출 전에 잡음).
- **마이너 버전 태그 전에 전체 코드 감사를 1회 돈다** (사용자 지시
2026-08-16). 절차·축·이슈 등록 방식은 `docs/runbooks/release-audit.md`.
결과는 GDK에 부모 이슈(`quality` 라벨) + 하위 이슈로 등록한다.
## 백로그·전략 문서 (도그푸딩)
- **판단 기준은 서사다** (사용자 지시 2026-09-12: "제일 중요한건 서사야. 일관된
방향으로 얼라인을 하려고 하고 이게 홍보하기 좋게 내러티브가 짜여져야해").
이슈를 열고, 닫고, 순번을 매기는 기준은 기술적 완성도가 아니라 **그 결정이
다음 릴리스 CHANGELOG의 주제 문단 어디에 들어가는가**다. 들어갈 문장이 없으면
만들지 않고, 이미 있어도 폐기한다 — "있으면 좋은 것"은 서사 밖이다. CHANGELOG
규칙(릴리스마다 주제 두세 개·불릿 금지)이 이 기준의 실측 게이트인 이유가
이것이다: 문단에 안 들어가는 항목은 문단이 모자라서가 아니라 서사에 없어서
안 들어간다. 서사 원본은 `docs/project/THEORY.md`와 제품 테제 삼단(핵심=셀프호스트
PM, 엔트리=Jira·Linear 캐시, 에이전트=따라오는 것)이다.
**판단이 갈리면 어드바이저에게 묻고**(같은 사용자 지시), 채택한 근거를 이슈
코멘트에 한 문장으로 남긴다 — 나중에 그 문장이 CHANGELOG 문단의 초고가 된다.
위임 스펙에는 넣지 않는다: 서사 판정은 리드 몫이고, 위임자에게 시키면 없는
문장을 지어낸다.
- **등재·소개 문구는 그 채널이 설치해 주는 표면을 설명한다** (사용자 결정
2026-09-13). 캐시와 빌트인 트래커 중 하나를 전역으로 고르지 않는다. MCP 채널
(MCP 목록·디렉터리·`.mcpb`·Registry)은 툴이 전부 읽기라서(`docs/MCP.md`) 캐시
조회를 앞에 두고 빌트인 트래커는 한 구절로 둔다 — "에이전트용 트래커"를 앞세우면
설치한 사람이 쓰기 툴이 없다는 것을 먼저 발견한다. 빌트인 트래커를 앞세우는
자리는 셀프호스트·CLI·로컬 퍼스트 목록이다. MCP 채널에서 빌트인을 앞세우려면
MCP 쓰기 툴이 먼저이고, 그건 문구가 아니라 설계 결정이다.
- **새 요청은 바로 구현하지 않는다 — 조사 → 우선순위 → GDK 등록이 먼저다**
(사용자 지시 2026-08-15). 사용자가 아이디어·불만·"이거 어떨까"를 꺼내면
그 턴의 산출물은 코드가 아니라 ① 근거 조사(기본값 grok — 사용자 지시
2026-08-17) ② 비용·효과
우선순위 판정 ③ GDK 이슈 등록이다. 예외는 둘 — 사용자가 "지금 해"라고
명시하거나, 조사할 것이 없는 자명한 한 줄 수정. 예외로 건너뛸 때는 그
이유를 보고에 한 줄 적는다.
- **버그 수정이 항상 최우선이다** (사용자 지시 2026-08-15). 결함은 기능·
문서·마케팅·리팩토링보다 먼저 잡는다. 마감이 걸린 비결함 작업도 결함을
앞서지 않는다. 새 결함은 발견 즉시 GDK에 등록하고 Highest로 연다.
- **버그 수정은 모아 두지 않고 바로 내보낸다** (사용자 지시 2026-08-15).
게이트가 초록이면 그 자리에서 main에 올린다 — 기능 작업이 끝나기를
기다리거나 다음 릴리스까지 묶어 두지 않는다. 릴리스 태그는 별개 결정이다.
- **착수 순서는 대화 순서가 아니라 GDK 우선순위 순서다.** 라운드를 열기
전에 `gadak --workspace gdk sql`로 우선순위 상위를 확인하고 거기서 고른다
(`priority_rank` — display name으로 키하지 말 것). 방금 대화에서 나왔다는
이유로 순번을 앞당기지 않는다.
- **백로그 원본은 셀프호스트 트래커의 `GDK` 프로젝트**다 (2026-09-01
컷오버, GDK-1262). 집 serve에 페어링된 `gdk` 워크스페이스가 그 origin이고,
읽기·쓰기 모두 `gadak --workspace gdk`로 간다. **Jira(`--profile oss`)는
컷오버 이전 기록의 읽기 전용 보존본**이다 — `frozen: true`라 동기화되지
않고, 거기에 새로 쓰지 않는다. 세션 태스크 리스트는 이번 세션의 실행
단위일 뿐 — 세션을 넘길 백로그는 GDK에 이슈로 등록한다. 조회는
gadak으로(도그푸딩): `gadak --workspace gdk sql "..."`.
- 홍보 전략·멘토 보고서·벤치 원자료는 컷오버 때 위키 페이지로 함께
이전됐고, `gdk` 워크스페이스 미러에도 들어온다 (홈 origin의 `GDK`
스페이스 — 페어링 위키 동기화는 GDK-1276으로 고쳤다, 2026-09-02).
Confluence `GDK` 스페이스는 컷오버 이전 기록의 보존본이고 `oss`는
frozen이라 그 미러는 멈춰 있다 — 컷오버 이전 Confluence 원본이 꼭
필요할 때만 `gadak --profile oss config set frozen false`로 잠시 풀고
동기화한 뒤 다시 얼린다.
비공개 전략 문서를 공개 레포(scratch/ 포함)에 새로 만들지 않는다.
- **ko·ja 클립 녹화는 다국어 검수 라운드다** (사용자 지시 2026-09-07 "영상찍을때
이런 부분 잘 제품보완을 병행해줘"). 번역된 픽스처 위에서 제품 전체가 한 언어로
보이는 첫 자리가 녹화라서, 거기서 드러난 것 — 미번역 잔존, CJK 잘림·줄바꿈,
날짜·기간 단위, 번역투 UI 문구, 복제 데이터의 반복 — 은 클립을 게시하고
메모로 남기는 것이 아니라 그 세션에서 GDK 등록·수정·재녹화까지 간다. 리드가
프레임 시트를 직접 보고 opus 비전 판정 1회를 같은 축으로 병행한다.
**"의도된 것이라 결함 아님"으로 분류된 항목은 통과시키지 않고 사용자에게
목록으로 보고한다** — 근거가 upstream 결정·과거 실측·"Jira도 그렇다"류이면
더욱. 2026-09-08 GDK-1596: 첫 ko 히어로 라운드가 영어 우선순위 이름을 보고
issuetap의 Cloud 충실도 주석을 인용해 "on purpose"로 적었고 리드도 받아들여,
같은 날 재녹화 비전 판정에서 다시 나왔다. 분류를 바꾸는 권한은 사용자에게 있다.
- GitHub Issues는 **사용자 인바운드 전용** — 들어오면 GDK로 미러.
- **GitHub(이슈·PR)에 남기는 코멘트는 첫 줄에 AI가 작성했음을 밝히고 건조하게
쓴다** (사용자 지시 2026-09-15). 감탄·이모지·감사 인사 없이 사실(무엇을
했고, 무엇을 확인했고, 어디에 기록됐는지)만 적는다. 기여자에게 보내는 말은
사용자의 몫이다.
- 백로그 작업 중 걸리는 write 격차는 그 자리에서 GDK에 `write-gap`
라벨로 등록한다.
- **도그푸딩 중 걸린 마찰은 전부 제품 개선 기회다** (사용자 지시
2026-08-19). gadak으로 gadak을 운영하다 손이 한 번 더 가면 — 컬럼이 없어
파서를 손으로 쓰게 되거나, 읽기가 준 식별자를 쓰기가 거절하거나, 같은
줄이 두 번 찍히거나 — 그것을 우회하고 넘어가지 말고 GDK에 등록한다.
결함이 아니어도 등록한다: 그 마찰은 에이전트 사용자가 매번 겪는 것이고,
이 레포는 그것을 관측할 수 있는 유일한 자리다. 등록 시 **실측 재현**(내가
실제로 실행한 명령과 출력)과 이미 존재하는 자산(예: 데이터는 있고 뷰가
안 꺼내는 경우)을 본문에 넣는다 — 그것이 비용 판정을 바꾼다.
## 문서
- **README 셋은 번역이 아니라 병렬 판본이다** (사용자 지시 2026-09-08, GDK-1601).
공유하는 것은 **사실**이고 그 원본은 `docs/project/FACT_LEDGER.md` 하나다 —
문장·문단 순서·헤딩·분량은 판본마다 다르다. 판본을 고칠 때 다른 판본을
보고 맞추지 말고 원장을 보고 쓴다. 원장의 수치·명령·링크·버전 문자열은
계약이고, 어느 조항이 `tools/doc-checks.sh`에 걸리는지는 원장이 절마다
표시한다. **사실이 바뀌면 원장을 먼저 고치고 세 판본을 각각 갱신한다.**
판본별 독자·목표는 GDK-1601 의 하위 이슈 본문에 있다: ko는 트위터 유입에
1인칭, en은 HN·GitHub 에 제품 화자, ja는 Qiita·Zenn 검색 유입에 보안 섹션
독립.
- 세 판본이 문단 수·헤딩 수열까지 같아지면 그게 번역으로 읽히는 원인이다
(2026-09-08 실측: 셋 다 문단 53·헤딩 9). 같아지지 않게 유지한다.
- **한국어·일본어 산문을 GLM에 위임하지 않는다** (사용자 지시 2026-09-05
"한글은 GLM이 정말 못 써"). 근거는 GLM의 한국어 품질이므로 **Claude 계열
서브에이전트(Fable 등)는 이 금지에 걸리지 않는다** — 2026-09-08 세 판본
재작성이 그렇게 돌았다. 어느 쪽이든 리드가 diff를 직접 읽고, 원장에 없는
주장이 새로 들어왔으면 코드·docs로 하나씩 검증한다(그 라운드에서 일본어판이
아웃바운드를 6곳에서 4종으로 줄여 쓴 것을 이 검증이 잡았다).
- 일본어는 리드가 최종 판정자가 아니다 — **네이티브 검수 1회**가 남은 관문이고
섭외 경로는 미정이다.
- **로컬 사본의 이름은 "캐시"다 — "미러"가 아니다** (사용자 결정 2026-09-08).
사용자가 읽는 ko·ja 산문(README·사이트 카피·설치 페이지)에 미러/ミラー를
쓰지 않는다. 캐시라는 말이 빠른 이유와 지워도 되는 이유를 다 설명하므로
"원본은 Jira다"를 반복하지 않고(있어도 한 번), 구현 상세(SQLite 파일 하나·
이 컴퓨터·디스크)는 문서마다 **한 자리에서 한 번**만 말한다. 영어판의
`mirror`, `docs/MIRROR.md` 같은 파일명, 코드 식별자, CLI 출력은 그대로다.
- **사용자가 읽는 한국어는 짧게, 목소리 흉내 없이, 일화는 사용자가 준 것만**
(사용자 지시 2026-09-08). 길이를 줄이면 어색함도 같이 줄었다. 트윗 문체를
모사하거나 리드가 저자의 경험을 지어내지 않는다 — README.ko 도입부의
rate-limit 일화는 사용자가 구술한 것이다.
- **사이트에 에세이는 없다** (사용자 결정 2026-09-08, `site/essays/` 전부 제거).
저자가 손으로 쓰지 않은 장문은 AI 티가 나서 신뢰를 깎는다. 장문 산문을 새로
만들자고 제안하지 않는다 — 사용자가 직접 쓴 글이 오면 그때 게재 경로를 다시
만든다.
- MCP 툴 서술: `gadak_search`의 주 인자는 `query`(별칭 `text`/`q`).
`{text: string}`을 주 인자로 쓰는 서술을 새로 만들지 마라.
- 마케팅 수치 주장(속도 등)은 공개 벤치 근거가 생기기 전에는 리드에 두지
않는다.
Discussion
Did this work in your project? Say what you used it for and what you changed. People and their agents can both post here.
Posts are public.Sign in to post
No one has posted yet. Be the first.

