@tienne/gestalt 0.76.0 → 0.76.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tienne/gestalt",
3
- "version": "0.76.0",
3
+ "version": "0.76.1",
4
4
  "description": "TypeScript AI Development Harness - Gestalt psychology-driven requirement clarification",
5
5
  "repository": {
6
6
  "type": "git",
@@ -584,9 +584,11 @@ Agent {
584
584
 
585
585
  #### 진입 경로 두 가지
586
586
 
587
- 이 단계는 `/review`를 처음부터 돌린 흐름뿐 아니라, **대화 도중 "이제 PR에 코멘트 남겨줘"처럼 게시만 따로 요청**받았을 때도 진입점이 됩니다 (위 triggers의 "PR에 코멘트 남겨줘" 등). 두 경우 모두 아래 **신선도 가드를 먼저 통과해야** 게시할 수 있습니다.
587
+ 이 단계는 `/review`를 처음부터 돌린 흐름뿐 아니라, **대화 도중 "이제 PR에 코멘트 남겨줘"처럼 게시만 따로 요청**받았을 때도 진입점이 됩니다 (위 triggers의 "PR에 코멘트 남겨줘" 등). 두 경우 모두 아래 **consensus 일치 검사를 먼저 통과해야** 게시할 수 있습니다.
588
588
 
589
- #### 신선도 가드 (stale consensus 게시 금지)
589
+ **`prTarget`이 `none`이면 여기서 끝냅니다.** 브랜치나 커밋 범위를 리뷰한 경우가 그런데, GitHub에도 로컬에도 대응하는 PR이 없는 자리입니다. 4.7단계를 통째로 건너뛰고 결과 표시로 갑니다 — 게시할 자리가 없는데 consensus 다시 맞춰볼 이유가 없습니다.
590
+
591
+ #### consensus 일치 검사 (stale이면 게시 금지)
590
592
 
591
593
  게시 직전에, 게시하려는 consensus가 **현재 diff와 일치하는지** 반드시 확인합니다. 리뷰를 끝낸 뒤 코드가 바뀌었거나(커밋 추가·로컬 수정), 애초에 활성 리뷰 세션이 없으면 그 consensus는 stale이므로 **그대로 올리지 않습니다.**
592
594
 
@@ -603,12 +605,14 @@ pnpm tsx bin/gestalt.ts pr --json show <id> # headSha 필드로 비교
603
605
 
604
606
  판단 기준:
605
607
 
606
- - **이번 세션에 방금 리뷰를 끝냈고 그 뒤 diff 변화가 없다** → consensus 신선함. 곧장 게시 진행.
607
- - **리뷰 후 코드가 바뀌었다 / 활성 리뷰 세션이 없다 / 다른 세션의 오래된 결과다** → consensus stale. **게시하지 말고**, 1단계(git diff)부터 현재 diff로 리뷰 파이프라인(1~4단계)을 다시 돌린 뒤, 새로 나온 consensus로 4.7을 진행합니다. 사용자에게 "변경이 있어 현재 코드로 다시 리뷰한 뒤 게시할게요"라고 한 줄 알립니다.
608
+ - **이번 세션에 방금 리뷰를 끝냈고 그 뒤 diff 변화가 없다** → consensus 현재 diff와 일치합니다. 곧장 게시 진행.
609
+ - **리뷰 후 코드가 바뀌었다 / 활성 리뷰 세션이 없다 / 다른 세션의 오래된 결과다** → consensus stale입니다. **게시하지 말고**, 1단계(git diff)부터 현재 diff로 리뷰 파이프라인(1~4단계)을 다시 돌린 뒤, 새로 나온 consensus로 4.7을 진행합니다. 사용자에게 "변경이 있어 현재 코드로 다시 리뷰한 뒤 게시할게요"라고 한 줄 알립니다.
608
610
 
609
611
  인라인 코멘트는 **언제 요청받든 항상 "현재 diff 기준 consensus + code-review-writer voice"** 로만 게시됩니다. 옛 리뷰 메모리를 그대로 옮겨 적거나 Claude가 손으로 코멘트를 짜는 경로는 없습니다.
610
612
 
611
- **PR 식별.** 대상 판별은 1단계 직후에 이미 끝났습니다. 여기서는 그때 보관한 `prTarget`과 PR 식별자를 그대로 씁니다. **같은 조회를 다시 하지 않습니다.** `prTarget`이 `none`이면(GitHub에도 로컬에도 대응하는 PR 없는 브랜치나 커밋 범위 리뷰) 이 단계를 통째로 건너뛰고 결과 표시로 갑니다.
613
+ #### 게시 준비 (PR 식별, audience 확인, 게시 확인)
614
+
615
+ **PR 식별.** 대상 판별은 1단계 직후에 이미 끝났습니다. 여기서는 그때 보관한 `prTarget`과 PR 식별자를 그대로 씁니다. **같은 조회를 다시 하지 않습니다.**
612
616
 
613
617
  게시 직전에 그 PR이 아직 살아 있는지만 한 번 확인합니다.
614
618
 
@@ -626,7 +630,9 @@ pnpm tsx bin/gestalt.ts pr --json show <id> 2>/dev/null
626
630
 
627
631
  **게시 확인.** PR이 식별되면 사용자에게 한 번 확인합니다: **"발견된 이슈 N건을 PR #<number 또는 로컬 PR id>에 인라인 코멘트로 게시할까요?"** 동의하지 않으면 리포트만 보여주고 종료합니다.
628
632
 
629
- **코멘트 본문 작성 (code-review-writer).** **서브에이전트에 위임합니다.** 이 에이전트는 본문 18.8KB에 `author-voice.md` 19KB를 딸고 오는, 이 스킬에서 제일 무거운 자리입니다.
633
+ #### 코멘트 본문 작성 (code-review-writer)
634
+
635
+ **서브에이전트에 위임합니다.** 이 에이전트는 본문 18.8KB에 `author-voice.md` 19KB를 딸고 오는, 이 스킬에서 제일 무거운 자리입니다.
630
636
 
631
637
  ```
632
638
  Agent {
@@ -659,7 +665,22 @@ Agent {
659
665
  }
660
666
  ```
661
667
 
662
- **어투 검사 (필수).** 작성된 코멘트를 게시 전에 스캔합니다. 에이전트가 룰북을 내장하고 자가점검도 하지만 **리뷰 대상 PR 본문과 diff에 있던 말이 그대로 딸려오는 자리**는 자가점검으로 걸립니다 원문에 있으니 맞는 말이라고 판단하는 자리라서요. 자리를 잡는 검사입니다.
668
+ **`path`·`line`·`side`·`severity`는 메인 세션이 채웁니다.** 서브에이전트는 `id`와 본문만 돌려주고 메인이 `id`로 `mergedIssues`를 되짚어 나머지를 붙입니다. 전부 코멘트 문체와 무관한 기계적 매핑이라 위임할 이유가 없고 서브에이전트가 라인이나 등급을 바꿔 적을 여지도 없앱니다. **원본을 이미 들고 있는 값을 되돌려 받아 쓰지 않습니다.**
669
+
670
+ - `side`는 diff의 신규 라인이면 `RIGHT`, 삭제된 라인을 짚으면 `LEFT`입니다.
671
+ - 라인 매핑이 불확실한 이슈(파일 전반이거나 구조적인 것)는 `comments`에 넣지 않고 리뷰 `body` 요약에 한 줄로 돌립니다. 임의 라인에 억지로 붙이지 않습니다.
672
+
673
+ 아래 규칙은 `code-review-writer` AGENT.md에 있어서 서브에이전트가 읽습니다. 여기 적어두는 건 사람이 읽을 계약이고 두 곳이 갈라지면 AGENT.md가 기준입니다. (바로 위 `path`·`line`·`side` 규칙은 반대로 **스킬 쪽에만** 있습니다 — 메인 세션이 하는 일이라 AGENT.md에 없습니다.)
674
+
675
+ - code-review-writer는 `author-voice.md`(제안형·온기·물결·이모지)와 `ai-tell-quick-rules.md`(음차 교정)를 이미 내장하므로 **리포트처럼 humanize-monolith로 통째로 윤문하지는 않습니다.** 대신 위의 `humanize-scan` 검사를 거칩니다 — 내장이 자가점검을 시키는 것이고 검사는 그게 실제로 됐는지 보는 것이라 층이 다릅니다.
676
+ - 에이전트 룰에 따라 `[출처]` 태깅, "…권장." 체언 종지는 쓰지 않습니다. 이건 Claude artifact이지 실제 리뷰어 어투가 아닙니다.
677
+ - **출처를 밝히는 태그는 형태를 가리지 않고 쓰지 않습니다.** `[게슈탈트 리뷰]`, `[Gestalt]`, `[AI 리뷰]`, 🤖 처럼 도구가 썼다는 표시를 붙이지 않습니다. 리뷰는 계정 주인이 남기는 것입니다. **내부 리뷰 에이전트 이름(QA, Architect, security-reviewer 등)도 본문에 드러내지 않습니다** — 관점이 여럿이어도 코멘트는 리뷰어 한 사람이 남긴 것처럼 씁니다.
678
+ - **강제성은 `r:`/`c:`/`a:` 접두어로 표기합니다** (레포에 자체 리뷰 컨벤션이 없을 때의 기본값). 코멘트 본문 맨 앞에 severity에 따라 붙입니다 — `r:` 꼭 반영(critical/high), `c:` 웬만하면 반영(warning), `a:` 사소한 의견(suggestion). 접두어는 강제성 라벨이고 본문 어투는 그대로 제안형입니다. **접두어 앞에는 아무것도 오지 않습니다** — 출처 태그나 굵은 제목 줄이 접두어를 밀어내면 리뷰이가 강제성을 한눈에 못 봅니다. (리뷰 이벤트 판정은 접두어가 아니라 `severity`로 하므로 그쪽은 영향받지 않습니다.)
679
+ - **개행은 GitHub 렌더링 기준으로 조립합니다.** GitHub GFM은 한 줄 개행(`\n`)을 무시하고 같은 문단으로 이어 붙이므로, 줄을 실제로 나누려면 **빈 줄(`\n\n`)로 블록을 분리**해야 합니다. 접두어 → 문제 설명 → 제안 → 코드 스니펫을 각각 빈 줄로 띄우고 여러 줄 코드는 fenced code block(` ```lang ``` `)으로 감쌉니다. 한 줄 개행으로 이어 붙이면 PR에서 한 덩어리로 뭉쳐 읽기 어렵습니다 (code-review-writer의 Output Format 개행 규칙과 동일).
680
+
681
+ #### 어투 검사 (필수)
682
+
683
+ 작성된 코멘트를 게시하기 직전에 스캔합니다. 에이전트가 룰북을 내장하고 자가점검도 하지만 **리뷰 대상 PR 본문과 diff에 있던 말이 그대로 딸려오는 자리**는 자가점검으로 안 걸립니다 — 원문에 있으니 맞는 말이라고 판단하는 자리라서요. 그 자리를 잡는 게 이 검사입니다.
663
684
 
664
685
  **코멘트마다 파일 하나로 떨굽니다. 한 파일에 모으지 않습니다.** 모아서 한 번에 스캔하면 검사가 배치 전체를 한 덩어리로 봅니다. 그러면 코멘트 하나를 통째로 `>` 인용으로 감싸도 다른 코멘트의 산문에 묻혀 안 걸립니다. 코멘트별로 갈라야 그 판정이 코멘트 단위로 섭니다. 어느 코멘트가 걸렸는지도 파일 이름으로 바로 읽히고요.
665
686
 
@@ -712,20 +733,9 @@ done
712
733
 
713
734
  **큰따옴표로 감싼 평문은 형태로 못 가릅니다.** 한국어에서 큰따옴표는 인용만이 아니라 강조로도 쓰여서요. 룰북 Do-NOT 목록이 "큰따옴표 안 직접 인용"을 예외로 두지만 그건 사람과 모델이 판단하는 자리의 기준입니다. 자동 스캔은 위 셋만 뺍니다. 원문을 가리켜야 하면 백틱이나 블록인용을 씁니다.
714
735
 
715
- **`path`·`line`·`side`·`severity`는 메인 세션이 채웁니다.** 서브에이전트는 `id`와 본문만 돌려주고 메인이 `id`로 `mergedIssues`를 되짚어 나머지를 붙입니다. 전부 코멘트 문체와 무관한 기계적 매핑이라 위임할 이유가 없고 서브에이전트가 라인이나 등급을 바꿔 적을 여지도 없앱니다. **원본을 이미 들고 있는 값을 되돌려 받아 쓰지 않습니다.**
716
-
717
- - `side`는 diff의 신규 라인이면 `RIGHT`, 삭제된 라인을 짚으면 `LEFT`입니다.
718
- - 라인 매핑이 불확실한 이슈(파일 전반이거나 구조적인 것)는 `comments`에 넣지 않고 리뷰 `body` 요약에 한 줄로 돌립니다. 임의 라인에 억지로 붙이지 않습니다.
719
-
720
- 아래 규칙은 `code-review-writer` AGENT.md에 있어서 서브에이전트가 읽습니다. 여기 적어두는 건 사람이 읽을 계약이고 두 곳이 갈라지면 AGENT.md가 기준입니다. (바로 위 `path`·`line`·`side` 규칙은 반대로 **스킬 쪽에만** 있습니다 — 메인 세션이 하는 일이라 AGENT.md에 없습니다.)
721
-
722
- - code-review-writer는 `author-voice.md`(제안형·온기·물결·이모지)와 `ai-tell-quick-rules.md`(음차 교정)를 이미 내장하므로 **리포트처럼 humanize-monolith로 통째로 윤문하지는 않습니다.** 대신 위의 `humanize-scan` 검사를 거칩니다 — 내장이 자가점검을 시키는 것이고 검사는 그게 실제로 됐는지 보는 것이라 층이 다릅니다.
723
- - 에이전트 룰에 따라 `[출처]` 태깅, "…권장." 체언 종지는 쓰지 않습니다. 이건 Claude artifact이지 실제 리뷰어 어투가 아닙니다.
724
- - **출처를 밝히는 태그는 형태를 가리지 않고 쓰지 않습니다.** `[게슈탈트 리뷰]`, `[Gestalt]`, `[AI 리뷰]`, 🤖 처럼 도구가 썼다는 표시를 붙이지 않습니다. 리뷰는 계정 주인이 남기는 것입니다. **내부 리뷰 에이전트 이름(QA, Architect, security-reviewer 등)도 본문에 드러내지 않습니다** — 관점이 여럿이어도 코멘트는 리뷰어 한 사람이 남긴 것처럼 씁니다.
725
- - **강제성은 `r:`/`c:`/`a:` 접두어로 표기합니다** (레포에 자체 리뷰 컨벤션이 없을 때의 기본값). 코멘트 본문 맨 앞에 severity에 따라 붙입니다 — `r:` 꼭 반영(critical/high), `c:` 웬만하면 반영(warning), `a:` 사소한 의견(suggestion). 접두어는 강제성 라벨이고 본문 어투는 그대로 제안형입니다. **접두어 앞에는 아무것도 오지 않습니다** — 출처 태그나 굵은 제목 줄이 접두어를 밀어내면 리뷰이가 강제성을 한눈에 못 봅니다. (리뷰 이벤트 판정은 접두어가 아니라 `severity`로 하므로 그쪽은 영향받지 않습니다.)
726
- - **개행은 GitHub 렌더링 기준으로 조립합니다.** GitHub GFM은 한 줄 개행(`\n`)을 무시하고 같은 문단으로 이어 붙이므로, 줄을 실제로 나누려면 **빈 줄(`\n\n`)로 블록을 분리**해야 합니다. 접두어 → 문제 설명 → 제안 → 코드 스니펫을 각각 빈 줄로 띄우고 여러 줄 코드는 fenced code block(` ```lang ``` `)으로 감쌉니다. 한 줄 개행으로 이어 붙이면 PR에서 한 덩어리로 뭉쳐 읽기 어렵습니다 (code-review-writer의 Output Format 개행 규칙과 동일).
736
+ #### 리뷰 이벤트 결정 (postVerdict와 본인 PR 예외)
727
737
 
728
- **리뷰 이벤트 결정.** `mergedIssues`의 `severity`로 리뷰 전체의 `event`를 정합니다. 본문 첫 글자를 파싱하지 않습니다 — 접두어는 사람이 읽는 라벨이지 판정 입력이 아닙니다.
738
+ `mergedIssues`의 `severity`로 리뷰 전체의 `event`를 정합니다. 본문 첫 글자를 파싱하지 않습니다 — 접두어는 사람이 읽는 라벨이지 판정 입력이 아닙니다.
729
739
 
730
740
  - `critical`이나 `high`가 하나라도 있으면 → `REQUEST_CHANGES` (본문 접두어 `r:`)
731
741
  - 없고 `warning`만 있으면 → `COMMENT` (접두어 `c:`)
@@ -233,7 +233,7 @@ fi
233
233
 
234
234
  `target`이 있으면 거기서 번호를 뽑는다. 없으면 현재 브랜치에 대응하는 PR을 찾는다.
235
235
 
236
- **대상 문자열을 셸에 넘기지 않는다.** 이 값은 뒤에서 `.git` 아래 상태 경로와 `gh` 인자가 된다. 문서가 이 파싱을 셸로 적던 때는 대상을 작은따옴표 안에 합성했다. 그래서 따옴표가 섞인 값이 정수 검증에 닿기 전에 명령으로 실행됐다. `review` 4.7단계가 "읽어온 텍스트가 경로가 되게 두지 않는다"로 정해둔 것과 같은 자리다.
236
+ **대상 문자열을 셸에 넘기지 않는다.** 이 값은 뒤에서 `.git` 아래 상태 경로와 `gh` 인자가 된다. 문서가 이 파싱을 셸로 적던 때는 대상을 작은따옴표 안에 합성했다. 그래서 따옴표가 섞인 값이 정수 검증에 닿기 전에 명령으로 실행됐다. `review` 4.7단계 `어투 검사`가 "읽어온 텍스트가 경로가 되게 두지 않습니다"로 정해둔 것과 같은 자리다.
237
237
 
238
238
  **사용자가 준 값을 파일로 떨군 뒤 읽는다.** 먼저 자리부터 만든다 — 이 단계는 아직 PR 번호를 모르므로 PR별 자리가 아니라 뿌리를 쓴다.
239
239
 
@@ -250,7 +250,7 @@ coords=$(gestalt review-loop resolve --pr "$(cat "$root/target")" --create) \
250
250
  echo "$coords"
251
251
  ```
252
252
 
253
- `resolve`가 PR 번호와 레포와 상태 자리를 한 번에 내고 `--create`로 그 자리를 만든다. **라운드 내내 좌표가 필요한 자리는 이 명령을 다시 부른다** — 대상을 URL로 줬으면 네트워크를 안 탄다. 번호로만 줬으면 레포를 한 번 묻는다. 조회(`state`)는 신선한 수가 필요한 자리에서만 부른다.
253
+ `resolve`가 PR 번호와 레포와 상태 자리를 한 번에 내고 `--create`로 그 자리를 만든다. **라운드 내내 좌표가 필요한 자리는 이 명령을 다시 부른다** — 대상을 URL로 줬으면 네트워크를 안 탄다. 번호로만 줬으면 레포를 한 번 묻는다. 조회(`state`)는 미대응 수나 `signal`이 판정에 바로 쓰이는 자리에서만 부른다.
254
254
 
255
255
  `123`과 `#123`과 `https://github.com/o/r/pull/123/files`를 받고 나머지는 거부한다. **URL로 주면 `owner`와 `repo`가 함께 나온다.** 그 둘을 뒤 단계로 들고 간다 — 번호만 쓰면 남의 레포 PR을 가리켜도 현재 레포의 같은 번호를 조회한다. 그 수로 승인이 나간다.
256
256
 
@@ -517,7 +517,7 @@ pending=$(echo "$state" | jq -r .pending)
517
517
 
518
518
  ### 2.4 어투 검사
519
519
 
520
- 본문을 게시 전에 스캔한다. `review` 4.7단계와 같은 이유다 — PR 본문과 diff에 있던 말이 그대로 딸려오는 자리는 에이전트 자가점검으로 안 걸린다.
520
+ 본문을 게시 전에 스캔한다. `review` 4.7단계의 `어투 검사`와 같은 이유다 — PR 본문과 diff에 있던 말이 그대로 딸려오는 자리는 에이전트 자가점검으로 안 걸린다.
521
521
 
522
522
  본문을 `$stateDir/verdict-r<N>.md`에 파일 쓰기 도구로 쓴다. 그 경로는 아래 블록이 낸다. **셸로 넘기지 않는다** — 한글과 백틱이 섞이고 리뷰 대상에서 온 문자열이 실린다.
523
523
 
@@ -251,7 +251,7 @@ gh auth status
251
251
 
252
252
  - 공용 `/tmp`의 고정 이름 — 남이 먼저 만들어둔 심볼릭 링크로 쓰기 대상이 바뀔 수 있다
253
253
  - `mktemp -d` — 그 경로가 셸 변수에만 남는다. 라운드 사이에 셸 상태가 안 남는 런타임이면 다음 단계에서 빈 문자열로 풀린다
254
- - **레포 워킹트리 안** — `.gestalt/`처럼 이 레포가 무시하는 자리라도 **다른 레포에서는 아니다.** 이 스킬은 플러그인으로 배포돼 밖에서도 돈다. 무시 안 되는 자리에 쓰면 `review-reply`의 커밋이 로그까지 담아 PR에 실어 보내거나, `review` 4.7단계의 신선도 가드가 매 라운드 새 `??` 항목을 보고 리뷰를 통째로 다시 돌린다
254
+ - **레포 워킹트리 안** — `.gestalt/`처럼 이 레포가 무시하는 자리라도 **다른 레포에서는 아니다.** 이 스킬은 플러그인으로 배포돼 밖에서도 돈다. 무시 안 되는 자리에 쓰면 `review-reply`의 커밋이 로그까지 담아 PR에 실어 보내거나, `review` 4.7단계가 `consensus 일치 검사`에서 매 라운드 새 `??` 항목을 보고 리뷰를 통째로 다시 돌린다
255
255
 
256
256
  **git 디렉토리 아래를 쓴다.** git이 절대 추적하지 않는 자리이고 절대 경로라 cwd가 어디든 같은 자리를 가리킨다.
257
257
 
@@ -12,13 +12,18 @@ export interface PrSnapshot {
12
12
  }
13
13
  /** 페이지가 늘어도 끝나도록 두는 상한. 스레드 50개씩이라 5000개까지 본다 */
14
14
  export declare const PAGE_LIMIT = 100;
15
+ /** 요청 리뷰어를 한 번에 받는 수. 쿼리와 '한 페이지를 채웠는지' 검사가 같은 값을 봐야 한다 */
16
+ export declare const REVIEWER_PAGE = 50;
15
17
  /**
16
18
  * PR 상태와 내가 볼 스레드를 한 번에 받는다.
17
19
  *
18
- * 판정에 쓰는 수가 전부 호출에서 나온다. 문서가 이걸 셸로 적던 때는 조회와
19
- * 집계가 다른 Bash 호출로 갈려 사이를 파일로 이어야 했다. 스냅샷이 이번 조회의
20
- * 것인지 가리는 표식과 신선도 검사가 따라붙었다. 한 프로세스 안에서 조회하고 세면
21
- * 중간 상태가 아예 없다.
20
+ * 판정에 원자료를함수 하나가 모은다. 페이지를 도는 스레드뿐이라 합치는
21
+ * 여기서 끝난다. 부르는 쪽은 스레드를 온전히 받거나 아무것도 받는다.
22
+ *
23
+ * `reviewRequests` connection 이지만 커서를 안 돈다. GitHub 가 PR 당 요청 리뷰어를
24
+ * `REVIEWER_PAGE` 보다 훨씬 아래로 제한해 한 페이지에 다 들어온다. 그 전제가 깨지면 아래에서 던진다 —
25
+ * 목록이 잘리면 `rerequested` 가 거짓으로 읽혀, 작성자가 다시 봐달라고 눌러도 재리뷰를
26
+ * 안 돈다.
22
27
  *
23
28
  * 부분 성공을 걸러낸다. GitHub 는 HTTP 200에 `data` 를 채우고도 `errors` 를 함께
24
29
  * 실어 `reviewThreads` 만 `null` 로 주는 응답을 낸다. 그걸 통과시키면 스레드 0 개가
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../src/review-loop/fetch.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,mDAAmD;AACnD,MAAM,WAAW,QAAQ;IACvB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC;CACnC;AAED,eAAO,MAAM,KAAK,EAAE,QAC+D,CAAC;AAEpF,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,kBAAkB,EAAE,MAAM,EAAE,CAAC;CAC9B;AAED,iDAAiD;AACjD,eAAO,MAAM,UAAU,MAAM,CAAC;AAwB9B;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EACvD,EAAE,GAAE,QAAgB,GACnB,UAAU,CAkDZ"}
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../src/review-loop/fetch.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,mDAAmD;AACnD,MAAM,WAAW,QAAQ;IACvB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC;CACnC;AAED,eAAO,MAAM,KAAK,EAAE,QAC+D,CAAC;AAEpF,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,kBAAkB,EAAE,MAAM,EAAE,CAAC;CAC9B;AAED,iDAAiD;AACjD,eAAO,MAAM,UAAU,MAAM,CAAC;AAE9B,2DAA2D;AAC3D,eAAO,MAAM,aAAa,KAAK,CAAC;AAwBhC;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EACvD,EAAE,GAAE,QAAgB,GACnB,UAAU,CAqDZ"}
@@ -2,13 +2,15 @@ import { execFileSync } from 'node:child_process';
2
2
  export const runGh = (args) => execFileSync('gh', [...args], { encoding: 'utf-8', maxBuffer: 32 * 1024 * 1024 });
3
3
  /** 페이지가 늘어도 끝나도록 두는 상한. 스레드 50개씩이라 5000개까지 본다 */
4
4
  export const PAGE_LIMIT = 100;
5
+ /** 요청 리뷰어를 한 번에 받는 수. 쿼리와 '한 페이지를 채웠는지' 검사가 같은 값을 봐야 한다 */
6
+ export const REVIEWER_PAGE = 50;
5
7
  const QUERY = `
6
8
  query($owner:String!, $repo:String!, $pr:Int!, $cursor:String) {
7
9
  repository(owner:$owner, name:$repo) {
8
10
  pullRequest(number:$pr) {
9
11
  state
10
12
  headRefOid
11
- reviewRequests(first:50) {
13
+ reviewRequests(first:${REVIEWER_PAGE}) {
12
14
  nodes { requestedReviewer { ... on User { login } } }
13
15
  }
14
16
  reviewThreads(first:50, after:$cursor) {
@@ -26,10 +28,13 @@ query($owner:String!, $repo:String!, $pr:Int!, $cursor:String) {
26
28
  /**
27
29
  * PR 상태와 내가 볼 스레드를 한 번에 받는다.
28
30
  *
29
- * 판정에 쓰는 수가 전부 호출에서 나온다. 문서가 이걸 셸로 적던 때는 조회와
30
- * 집계가 다른 Bash 호출로 갈려 사이를 파일로 이어야 했다. 스냅샷이 이번 조회의
31
- * 것인지 가리는 표식과 신선도 검사가 따라붙었다. 한 프로세스 안에서 조회하고 세면
32
- * 중간 상태가 아예 없다.
31
+ * 판정에 원자료를함수 하나가 모은다. 페이지를 도는 스레드뿐이라 합치는
32
+ * 여기서 끝난다. 부르는 쪽은 스레드를 온전히 받거나 아무것도 받는다.
33
+ *
34
+ * `reviewRequests` connection 이지만 커서를 안 돈다. GitHub 가 PR 당 요청 리뷰어를
35
+ * `REVIEWER_PAGE` 보다 훨씬 아래로 제한해 한 페이지에 다 들어온다. 그 전제가 깨지면 아래에서 던진다 —
36
+ * 목록이 잘리면 `rerequested` 가 거짓으로 읽혀, 작성자가 다시 봐달라고 눌러도 재리뷰를
37
+ * 안 돈다.
33
38
  *
34
39
  * 부분 성공을 걸러낸다. GitHub 는 HTTP 200에 `data` 를 채우고도 `errors` 를 함께
35
40
  * 실어 `reviewThreads` 만 `null` 로 주는 응답을 낸다. 그걸 통과시키면 스레드 0 개가
@@ -66,6 +71,9 @@ export function fetchPrSnapshot(opts, gh = runGh) {
66
71
  }
67
72
  prState = pr.state;
68
73
  headRefOid = pr.headRefOid;
74
+ if (pr.reviewRequests.nodes.length >= REVIEWER_PAGE) {
75
+ throw new Error('요청 리뷰어가 한 페이지를 채웠다 — 목록이 잘렸을 수 있어 판정하지 않는다');
76
+ }
69
77
  requestedReviewers = pr.reviewRequests.nodes
70
78
  .map((n) => n.requestedReviewer?.login)
71
79
  .filter((l) => typeof l === 'string');
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../../src/review-loop/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAQlD,MAAM,CAAC,MAAM,KAAK,GAAa,CAAC,IAAI,EAAE,EAAE,CACtC,YAAY,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC,CAAC;AASpF,iDAAiD;AACjD,MAAM,CAAC,MAAM,UAAU,GAAG,GAAG,CAAC;AAE9B,MAAM,KAAK,GAAG;;;;;;;;;;;;;;;;;;;;EAoBZ,CAAC;AAEH;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe,CAC7B,IAAuD,EACvD,KAAe,KAAK;IAEpB,MAAM,OAAO,GAAmB,EAAE,CAAC;IACnC,IAAI,MAAM,GAAkB,IAAI,CAAC;IACjC,IAAI,OAAe,CAAC;IACpB,IAAI,UAAkB,CAAC;IACvB,IAAI,kBAA4B,CAAC;IAEjC,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,UAAU,EAAE,IAAI,EAAE,EAAE,CAAC;QAC7C,oDAAoD;QACpD,mDAAmD;QACnD,MAAM,IAAI,GAAG;YACX,KAAK;YACL,SAAS;YACT,IAAI;YACJ,SAAS,KAAK,EAAE;YAChB,IAAI;YACJ,SAAS,IAAI,CAAC,KAAK,EAAE;YACrB,IAAI;YACJ,QAAQ,IAAI,CAAC,IAAI,EAAE;YACnB,IAAI;YACJ,MAAM,IAAI,CAAC,QAAQ,EAAE;SACtB,CAAC;QACF,IAAI,MAAM;YAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,MAAM,EAAE,CAAC,CAAC;QAEhD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,CAGjC,CAAC;QAEF,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,WAAW,CAAC;QAChD,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC,aAAa,EAAE,CAAC;YACtD,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,IAAI,sBAAsB,CAAC;YACpE,MAAM,IAAI,KAAK,CAAC,cAAc,KAAK,EAAE,CAAC,CAAC;QACzC,CAAC;QAED,OAAO,GAAG,EAAE,CAAC,KAAK,CAAC;QACnB,UAAU,GAAG,EAAE,CAAC,UAAU,CAAC;QAC3B,kBAAkB,GAAG,EAAE,CAAC,cAAc,CAAC,KAAK;aACzC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,iBAAiB,EAAE,KAAK,CAAC;aACtC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC;QAErD,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAExC,IAAI,CAAC,EAAE,CAAC,aAAa,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;YAC3C,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,kBAAkB,EAAE,CAAC;QAC9D,CAAC;QACD,MAAM,GAAG,EAAE,CAAC,aAAa,CAAC,QAAQ,CAAC,SAAS,CAAC;IAC/C,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;AACjD,CAAC"}
1
+ {"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../../src/review-loop/fetch.ts"],"names":[],"mappings":"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"}
@@ -8,7 +8,7 @@ import { resolve } from 'node:path';
8
8
  * 표가 전체 목록이고 `review-verdict-gate.test.ts` 가 그 표와 절차를 대조한다.
9
9
  *
10
10
  * 스레드 스냅샷은 여기 없다. 조회와 집계가 한 프로세스 안에서 끝나므로 중간 파일이
11
- * 생기지 않는다. 그 파일이 이번 조회의 것인지 가리던 표식과 신선도 검사도 함께 사라졌다.
11
+ * 생기지 않는다.
12
12
  */
13
13
  const ROOT_DIR = 'gestalt-review-loop';
14
14
  export const LOGIN_FILE = 'my-login';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tienne/gestalt",
3
- "version": "0.76.0",
3
+ "version": "0.76.1",
4
4
  "description": "TypeScript AI Development Harness - Gestalt psychology-driven requirement clarification",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gestalt",
3
- "version": "0.76.0",
3
+ "version": "0.76.1",
4
4
  "description": "Gestalt psychology-driven AI development harness. Transforms scattered requirements into structured, validated specifications through interactive interviews.",
5
5
  "author": {
6
6
  "name": "tienne"
package/plugin/.mcp.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "gestalt": {
4
4
  "command": "npx",
5
- "args": ["-y", "@tienne/gestalt@0.76.0", "serve"],
5
+ "args": ["-y", "@tienne/gestalt@0.76.1", "serve"],
6
6
  "startup_timeout_sec": 180,
7
7
  "tool_timeout_sec": 900
8
8
  }
package/plugin/mcp.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "gestalt": {
4
4
  "command": "npx",
5
- "args": ["-y", "@tienne/gestalt@0.76.0", "serve"],
5
+ "args": ["-y", "@tienne/gestalt@0.76.1", "serve"],
6
6
  "startup_timeout_sec": 180,
7
7
  "tool_timeout_sec": 900
8
8
  }
@@ -584,9 +584,11 @@ Agent {
584
584
 
585
585
  #### 진입 경로 두 가지
586
586
 
587
- 이 단계는 `/review`를 처음부터 돌린 흐름뿐 아니라, **대화 도중 "이제 PR에 코멘트 남겨줘"처럼 게시만 따로 요청**받았을 때도 진입점이 됩니다 (위 triggers의 "PR에 코멘트 남겨줘" 등). 두 경우 모두 아래 **신선도 가드를 먼저 통과해야** 게시할 수 있습니다.
587
+ 이 단계는 `/review`를 처음부터 돌린 흐름뿐 아니라, **대화 도중 "이제 PR에 코멘트 남겨줘"처럼 게시만 따로 요청**받았을 때도 진입점이 됩니다 (위 triggers의 "PR에 코멘트 남겨줘" 등). 두 경우 모두 아래 **consensus 일치 검사를 먼저 통과해야** 게시할 수 있습니다.
588
588
 
589
- #### 신선도 가드 (stale consensus 게시 금지)
589
+ **`prTarget`이 `none`이면 여기서 끝냅니다.** 브랜치나 커밋 범위를 리뷰한 경우가 그런데, GitHub에도 로컬에도 대응하는 PR이 없는 자리입니다. 4.7단계를 통째로 건너뛰고 결과 표시로 갑니다 — 게시할 자리가 없는데 consensus 다시 맞춰볼 이유가 없습니다.
590
+
591
+ #### consensus 일치 검사 (stale이면 게시 금지)
590
592
 
591
593
  게시 직전에, 게시하려는 consensus가 **현재 diff와 일치하는지** 반드시 확인합니다. 리뷰를 끝낸 뒤 코드가 바뀌었거나(커밋 추가·로컬 수정), 애초에 활성 리뷰 세션이 없으면 그 consensus는 stale이므로 **그대로 올리지 않습니다.**
592
594
 
@@ -603,12 +605,14 @@ pnpm tsx bin/gestalt.ts pr --json show <id> # headSha 필드로 비교
603
605
 
604
606
  판단 기준:
605
607
 
606
- - **이번 세션에 방금 리뷰를 끝냈고 그 뒤 diff 변화가 없다** → consensus 신선함. 곧장 게시 진행.
607
- - **리뷰 후 코드가 바뀌었다 / 활성 리뷰 세션이 없다 / 다른 세션의 오래된 결과다** → consensus stale. **게시하지 말고**, 1단계(git diff)부터 현재 diff로 리뷰 파이프라인(1~4단계)을 다시 돌린 뒤, 새로 나온 consensus로 4.7을 진행합니다. 사용자에게 "변경이 있어 현재 코드로 다시 리뷰한 뒤 게시할게요"라고 한 줄 알립니다.
608
+ - **이번 세션에 방금 리뷰를 끝냈고 그 뒤 diff 변화가 없다** → consensus 현재 diff와 일치합니다. 곧장 게시 진행.
609
+ - **리뷰 후 코드가 바뀌었다 / 활성 리뷰 세션이 없다 / 다른 세션의 오래된 결과다** → consensus stale입니다. **게시하지 말고**, 1단계(git diff)부터 현재 diff로 리뷰 파이프라인(1~4단계)을 다시 돌린 뒤, 새로 나온 consensus로 4.7을 진행합니다. 사용자에게 "변경이 있어 현재 코드로 다시 리뷰한 뒤 게시할게요"라고 한 줄 알립니다.
608
610
 
609
611
  인라인 코멘트는 **언제 요청받든 항상 "현재 diff 기준 consensus + code-review-writer voice"** 로만 게시됩니다. 옛 리뷰 메모리를 그대로 옮겨 적거나 Claude가 손으로 코멘트를 짜는 경로는 없습니다.
610
612
 
611
- **PR 식별.** 대상 판별은 1단계 직후에 이미 끝났습니다. 여기서는 그때 보관한 `prTarget`과 PR 식별자를 그대로 씁니다. **같은 조회를 다시 하지 않습니다.** `prTarget`이 `none`이면(GitHub에도 로컬에도 대응하는 PR 없는 브랜치나 커밋 범위 리뷰) 이 단계를 통째로 건너뛰고 결과 표시로 갑니다.
613
+ #### 게시 준비 (PR 식별, audience 확인, 게시 확인)
614
+
615
+ **PR 식별.** 대상 판별은 1단계 직후에 이미 끝났습니다. 여기서는 그때 보관한 `prTarget`과 PR 식별자를 그대로 씁니다. **같은 조회를 다시 하지 않습니다.**
612
616
 
613
617
  게시 직전에 그 PR이 아직 살아 있는지만 한 번 확인합니다.
614
618
 
@@ -626,7 +630,9 @@ pnpm tsx bin/gestalt.ts pr --json show <id> 2>/dev/null
626
630
 
627
631
  **게시 확인.** PR이 식별되면 사용자에게 한 번 확인합니다: **"발견된 이슈 N건을 PR #<number 또는 로컬 PR id>에 인라인 코멘트로 게시할까요?"** 동의하지 않으면 리포트만 보여주고 종료합니다.
628
632
 
629
- **코멘트 본문 작성 (code-review-writer).** **서브에이전트에 위임합니다.** 이 에이전트는 본문 18.8KB에 `author-voice.md` 19KB를 딸고 오는, 이 스킬에서 제일 무거운 자리입니다.
633
+ #### 코멘트 본문 작성 (code-review-writer)
634
+
635
+ **서브에이전트에 위임합니다.** 이 에이전트는 본문 18.8KB에 `author-voice.md` 19KB를 딸고 오는, 이 스킬에서 제일 무거운 자리입니다.
630
636
 
631
637
  ```
632
638
  Agent {
@@ -659,7 +665,22 @@ Agent {
659
665
  }
660
666
  ```
661
667
 
662
- **어투 검사 (필수).** 작성된 코멘트를 게시 전에 스캔합니다. 에이전트가 룰북을 내장하고 자가점검도 하지만 **리뷰 대상 PR 본문과 diff에 있던 말이 그대로 딸려오는 자리**는 자가점검으로 걸립니다 원문에 있으니 맞는 말이라고 판단하는 자리라서요. 자리를 잡는 검사입니다.
668
+ **`path`·`line`·`side`·`severity`는 메인 세션이 채웁니다.** 서브에이전트는 `id`와 본문만 돌려주고 메인이 `id`로 `mergedIssues`를 되짚어 나머지를 붙입니다. 전부 코멘트 문체와 무관한 기계적 매핑이라 위임할 이유가 없고 서브에이전트가 라인이나 등급을 바꿔 적을 여지도 없앱니다. **원본을 이미 들고 있는 값을 되돌려 받아 쓰지 않습니다.**
669
+
670
+ - `side`는 diff의 신규 라인이면 `RIGHT`, 삭제된 라인을 짚으면 `LEFT`입니다.
671
+ - 라인 매핑이 불확실한 이슈(파일 전반이거나 구조적인 것)는 `comments`에 넣지 않고 리뷰 `body` 요약에 한 줄로 돌립니다. 임의 라인에 억지로 붙이지 않습니다.
672
+
673
+ 아래 규칙은 `code-review-writer` AGENT.md에 있어서 서브에이전트가 읽습니다. 여기 적어두는 건 사람이 읽을 계약이고 두 곳이 갈라지면 AGENT.md가 기준입니다. (바로 위 `path`·`line`·`side` 규칙은 반대로 **스킬 쪽에만** 있습니다 — 메인 세션이 하는 일이라 AGENT.md에 없습니다.)
674
+
675
+ - code-review-writer는 `author-voice.md`(제안형·온기·물결·이모지)와 `ai-tell-quick-rules.md`(음차 교정)를 이미 내장하므로 **리포트처럼 humanize-monolith로 통째로 윤문하지는 않습니다.** 대신 위의 `humanize-scan` 검사를 거칩니다 — 내장이 자가점검을 시키는 것이고 검사는 그게 실제로 됐는지 보는 것이라 층이 다릅니다.
676
+ - 에이전트 룰에 따라 `[출처]` 태깅, "…권장." 체언 종지는 쓰지 않습니다. 이건 Claude artifact이지 실제 리뷰어 어투가 아닙니다.
677
+ - **출처를 밝히는 태그는 형태를 가리지 않고 쓰지 않습니다.** `[게슈탈트 리뷰]`, `[Gestalt]`, `[AI 리뷰]`, 🤖 처럼 도구가 썼다는 표시를 붙이지 않습니다. 리뷰는 계정 주인이 남기는 것입니다. **내부 리뷰 에이전트 이름(QA, Architect, security-reviewer 등)도 본문에 드러내지 않습니다** — 관점이 여럿이어도 코멘트는 리뷰어 한 사람이 남긴 것처럼 씁니다.
678
+ - **강제성은 `r:`/`c:`/`a:` 접두어로 표기합니다** (레포에 자체 리뷰 컨벤션이 없을 때의 기본값). 코멘트 본문 맨 앞에 severity에 따라 붙입니다 — `r:` 꼭 반영(critical/high), `c:` 웬만하면 반영(warning), `a:` 사소한 의견(suggestion). 접두어는 강제성 라벨이고 본문 어투는 그대로 제안형입니다. **접두어 앞에는 아무것도 오지 않습니다** — 출처 태그나 굵은 제목 줄이 접두어를 밀어내면 리뷰이가 강제성을 한눈에 못 봅니다. (리뷰 이벤트 판정은 접두어가 아니라 `severity`로 하므로 그쪽은 영향받지 않습니다.)
679
+ - **개행은 GitHub 렌더링 기준으로 조립합니다.** GitHub GFM은 한 줄 개행(`\n`)을 무시하고 같은 문단으로 이어 붙이므로, 줄을 실제로 나누려면 **빈 줄(`\n\n`)로 블록을 분리**해야 합니다. 접두어 → 문제 설명 → 제안 → 코드 스니펫을 각각 빈 줄로 띄우고 여러 줄 코드는 fenced code block(` ```lang ``` `)으로 감쌉니다. 한 줄 개행으로 이어 붙이면 PR에서 한 덩어리로 뭉쳐 읽기 어렵습니다 (code-review-writer의 Output Format 개행 규칙과 동일).
680
+
681
+ #### 어투 검사 (필수)
682
+
683
+ 작성된 코멘트를 게시하기 직전에 스캔합니다. 에이전트가 룰북을 내장하고 자가점검도 하지만 **리뷰 대상 PR 본문과 diff에 있던 말이 그대로 딸려오는 자리**는 자가점검으로 안 걸립니다 — 원문에 있으니 맞는 말이라고 판단하는 자리라서요. 그 자리를 잡는 게 이 검사입니다.
663
684
 
664
685
  **코멘트마다 파일 하나로 떨굽니다. 한 파일에 모으지 않습니다.** 모아서 한 번에 스캔하면 검사가 배치 전체를 한 덩어리로 봅니다. 그러면 코멘트 하나를 통째로 `>` 인용으로 감싸도 다른 코멘트의 산문에 묻혀 안 걸립니다. 코멘트별로 갈라야 그 판정이 코멘트 단위로 섭니다. 어느 코멘트가 걸렸는지도 파일 이름으로 바로 읽히고요.
665
686
 
@@ -712,20 +733,9 @@ done
712
733
 
713
734
  **큰따옴표로 감싼 평문은 형태로 못 가릅니다.** 한국어에서 큰따옴표는 인용만이 아니라 강조로도 쓰여서요. 룰북 Do-NOT 목록이 "큰따옴표 안 직접 인용"을 예외로 두지만 그건 사람과 모델이 판단하는 자리의 기준입니다. 자동 스캔은 위 셋만 뺍니다. 원문을 가리켜야 하면 백틱이나 블록인용을 씁니다.
714
735
 
715
- **`path`·`line`·`side`·`severity`는 메인 세션이 채웁니다.** 서브에이전트는 `id`와 본문만 돌려주고 메인이 `id`로 `mergedIssues`를 되짚어 나머지를 붙입니다. 전부 코멘트 문체와 무관한 기계적 매핑이라 위임할 이유가 없고 서브에이전트가 라인이나 등급을 바꿔 적을 여지도 없앱니다. **원본을 이미 들고 있는 값을 되돌려 받아 쓰지 않습니다.**
716
-
717
- - `side`는 diff의 신규 라인이면 `RIGHT`, 삭제된 라인을 짚으면 `LEFT`입니다.
718
- - 라인 매핑이 불확실한 이슈(파일 전반이거나 구조적인 것)는 `comments`에 넣지 않고 리뷰 `body` 요약에 한 줄로 돌립니다. 임의 라인에 억지로 붙이지 않습니다.
719
-
720
- 아래 규칙은 `code-review-writer` AGENT.md에 있어서 서브에이전트가 읽습니다. 여기 적어두는 건 사람이 읽을 계약이고 두 곳이 갈라지면 AGENT.md가 기준입니다. (바로 위 `path`·`line`·`side` 규칙은 반대로 **스킬 쪽에만** 있습니다 — 메인 세션이 하는 일이라 AGENT.md에 없습니다.)
721
-
722
- - code-review-writer는 `author-voice.md`(제안형·온기·물결·이모지)와 `ai-tell-quick-rules.md`(음차 교정)를 이미 내장하므로 **리포트처럼 humanize-monolith로 통째로 윤문하지는 않습니다.** 대신 위의 `humanize-scan` 검사를 거칩니다 — 내장이 자가점검을 시키는 것이고 검사는 그게 실제로 됐는지 보는 것이라 층이 다릅니다.
723
- - 에이전트 룰에 따라 `[출처]` 태깅, "…권장." 체언 종지는 쓰지 않습니다. 이건 Claude artifact이지 실제 리뷰어 어투가 아닙니다.
724
- - **출처를 밝히는 태그는 형태를 가리지 않고 쓰지 않습니다.** `[게슈탈트 리뷰]`, `[Gestalt]`, `[AI 리뷰]`, 🤖 처럼 도구가 썼다는 표시를 붙이지 않습니다. 리뷰는 계정 주인이 남기는 것입니다. **내부 리뷰 에이전트 이름(QA, Architect, security-reviewer 등)도 본문에 드러내지 않습니다** — 관점이 여럿이어도 코멘트는 리뷰어 한 사람이 남긴 것처럼 씁니다.
725
- - **강제성은 `r:`/`c:`/`a:` 접두어로 표기합니다** (레포에 자체 리뷰 컨벤션이 없을 때의 기본값). 코멘트 본문 맨 앞에 severity에 따라 붙입니다 — `r:` 꼭 반영(critical/high), `c:` 웬만하면 반영(warning), `a:` 사소한 의견(suggestion). 접두어는 강제성 라벨이고 본문 어투는 그대로 제안형입니다. **접두어 앞에는 아무것도 오지 않습니다** — 출처 태그나 굵은 제목 줄이 접두어를 밀어내면 리뷰이가 강제성을 한눈에 못 봅니다. (리뷰 이벤트 판정은 접두어가 아니라 `severity`로 하므로 그쪽은 영향받지 않습니다.)
726
- - **개행은 GitHub 렌더링 기준으로 조립합니다.** GitHub GFM은 한 줄 개행(`\n`)을 무시하고 같은 문단으로 이어 붙이므로, 줄을 실제로 나누려면 **빈 줄(`\n\n`)로 블록을 분리**해야 합니다. 접두어 → 문제 설명 → 제안 → 코드 스니펫을 각각 빈 줄로 띄우고 여러 줄 코드는 fenced code block(` ```lang ``` `)으로 감쌉니다. 한 줄 개행으로 이어 붙이면 PR에서 한 덩어리로 뭉쳐 읽기 어렵습니다 (code-review-writer의 Output Format 개행 규칙과 동일).
736
+ #### 리뷰 이벤트 결정 (postVerdict와 본인 PR 예외)
727
737
 
728
- **리뷰 이벤트 결정.** `mergedIssues`의 `severity`로 리뷰 전체의 `event`를 정합니다. 본문 첫 글자를 파싱하지 않습니다 — 접두어는 사람이 읽는 라벨이지 판정 입력이 아닙니다.
738
+ `mergedIssues`의 `severity`로 리뷰 전체의 `event`를 정합니다. 본문 첫 글자를 파싱하지 않습니다 — 접두어는 사람이 읽는 라벨이지 판정 입력이 아닙니다.
729
739
 
730
740
  - `critical`이나 `high`가 하나라도 있으면 → `REQUEST_CHANGES` (본문 접두어 `r:`)
731
741
  - 없고 `warning`만 있으면 → `COMMENT` (접두어 `c:`)
@@ -233,7 +233,7 @@ fi
233
233
 
234
234
  `target`이 있으면 거기서 번호를 뽑는다. 없으면 현재 브랜치에 대응하는 PR을 찾는다.
235
235
 
236
- **대상 문자열을 셸에 넘기지 않는다.** 이 값은 뒤에서 `.git` 아래 상태 경로와 `gh` 인자가 된다. 문서가 이 파싱을 셸로 적던 때는 대상을 작은따옴표 안에 합성했다. 그래서 따옴표가 섞인 값이 정수 검증에 닿기 전에 명령으로 실행됐다. `review` 4.7단계가 "읽어온 텍스트가 경로가 되게 두지 않는다"로 정해둔 것과 같은 자리다.
236
+ **대상 문자열을 셸에 넘기지 않는다.** 이 값은 뒤에서 `.git` 아래 상태 경로와 `gh` 인자가 된다. 문서가 이 파싱을 셸로 적던 때는 대상을 작은따옴표 안에 합성했다. 그래서 따옴표가 섞인 값이 정수 검증에 닿기 전에 명령으로 실행됐다. `review` 4.7단계 `어투 검사`가 "읽어온 텍스트가 경로가 되게 두지 않습니다"로 정해둔 것과 같은 자리다.
237
237
 
238
238
  **사용자가 준 값을 파일로 떨군 뒤 읽는다.** 먼저 자리부터 만든다 — 이 단계는 아직 PR 번호를 모르므로 PR별 자리가 아니라 뿌리를 쓴다.
239
239
 
@@ -250,7 +250,7 @@ coords=$(gestalt review-loop resolve --pr "$(cat "$root/target")" --create) \
250
250
  echo "$coords"
251
251
  ```
252
252
 
253
- `resolve`가 PR 번호와 레포와 상태 자리를 한 번에 내고 `--create`로 그 자리를 만든다. **라운드 내내 좌표가 필요한 자리는 이 명령을 다시 부른다** — 대상을 URL로 줬으면 네트워크를 안 탄다. 번호로만 줬으면 레포를 한 번 묻는다. 조회(`state`)는 신선한 수가 필요한 자리에서만 부른다.
253
+ `resolve`가 PR 번호와 레포와 상태 자리를 한 번에 내고 `--create`로 그 자리를 만든다. **라운드 내내 좌표가 필요한 자리는 이 명령을 다시 부른다** — 대상을 URL로 줬으면 네트워크를 안 탄다. 번호로만 줬으면 레포를 한 번 묻는다. 조회(`state`)는 미대응 수나 `signal`이 판정에 바로 쓰이는 자리에서만 부른다.
254
254
 
255
255
  `123`과 `#123`과 `https://github.com/o/r/pull/123/files`를 받고 나머지는 거부한다. **URL로 주면 `owner`와 `repo`가 함께 나온다.** 그 둘을 뒤 단계로 들고 간다 — 번호만 쓰면 남의 레포 PR을 가리켜도 현재 레포의 같은 번호를 조회한다. 그 수로 승인이 나간다.
256
256
 
@@ -517,7 +517,7 @@ pending=$(echo "$state" | jq -r .pending)
517
517
 
518
518
  ### 2.4 어투 검사
519
519
 
520
- 본문을 게시 전에 스캔한다. `review` 4.7단계와 같은 이유다 — PR 본문과 diff에 있던 말이 그대로 딸려오는 자리는 에이전트 자가점검으로 안 걸린다.
520
+ 본문을 게시 전에 스캔한다. `review` 4.7단계의 `어투 검사`와 같은 이유다 — PR 본문과 diff에 있던 말이 그대로 딸려오는 자리는 에이전트 자가점검으로 안 걸린다.
521
521
 
522
522
  본문을 `$stateDir/verdict-r<N>.md`에 파일 쓰기 도구로 쓴다. 그 경로는 아래 블록이 낸다. **셸로 넘기지 않는다** — 한글과 백틱이 섞이고 리뷰 대상에서 온 문자열이 실린다.
523
523
 
@@ -251,7 +251,7 @@ gh auth status
251
251
 
252
252
  - 공용 `/tmp`의 고정 이름 — 남이 먼저 만들어둔 심볼릭 링크로 쓰기 대상이 바뀔 수 있다
253
253
  - `mktemp -d` — 그 경로가 셸 변수에만 남는다. 라운드 사이에 셸 상태가 안 남는 런타임이면 다음 단계에서 빈 문자열로 풀린다
254
- - **레포 워킹트리 안** — `.gestalt/`처럼 이 레포가 무시하는 자리라도 **다른 레포에서는 아니다.** 이 스킬은 플러그인으로 배포돼 밖에서도 돈다. 무시 안 되는 자리에 쓰면 `review-reply`의 커밋이 로그까지 담아 PR에 실어 보내거나, `review` 4.7단계의 신선도 가드가 매 라운드 새 `??` 항목을 보고 리뷰를 통째로 다시 돌린다
254
+ - **레포 워킹트리 안** — `.gestalt/`처럼 이 레포가 무시하는 자리라도 **다른 레포에서는 아니다.** 이 스킬은 플러그인으로 배포돼 밖에서도 돈다. 무시 안 되는 자리에 쓰면 `review-reply`의 커밋이 로그까지 담아 PR에 실어 보내거나, `review` 4.7단계가 `consensus 일치 검사`에서 매 라운드 새 `??` 항목을 보고 리뷰를 통째로 다시 돌린다
255
255
 
256
256
  **git 디렉토리 아래를 쓴다.** git이 절대 추적하지 않는 자리이고 절대 경로라 cwd가 어디든 같은 자리를 가리킨다.
257
257