@tienne/gestalt 0.75.1 → 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.
Files changed (57) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.ko.md +3 -3
  3. package/README.md +3 -3
  4. package/dist/package.json +1 -1
  5. package/dist/plugin/skills/_shared/proactive-routing.md +1 -0
  6. package/dist/plugin/skills/local-pr/SKILL.md +1 -0
  7. package/dist/plugin/skills/review/SKILL.md +44 -21
  8. package/dist/plugin/skills/review-loop/CONTRACT.md +90 -0
  9. package/dist/plugin/skills/review-loop/SKILL.md +813 -0
  10. package/dist/plugin/skills/review-reply/SKILL.md +2 -2
  11. package/dist/plugin/skills/ship/SKILL.md +2 -1
  12. package/dist/src/cli/commands/review-loop.d.ts +45 -0
  13. package/dist/src/cli/commands/review-loop.d.ts.map +1 -0
  14. package/dist/src/cli/commands/review-loop.js +85 -0
  15. package/dist/src/cli/commands/review-loop.js.map +1 -0
  16. package/dist/src/cli/index.d.ts.map +1 -1
  17. package/dist/src/cli/index.js +27 -0
  18. package/dist/src/cli/index.js.map +1 -1
  19. package/dist/src/review-loop/fetch.d.ts +37 -0
  20. package/dist/src/review-loop/fetch.d.ts.map +1 -0
  21. package/dist/src/review-loop/fetch.js +88 -0
  22. package/dist/src/review-loop/fetch.js.map +1 -0
  23. package/dist/src/review-loop/index.d.ts +7 -0
  24. package/dist/src/review-loop/index.d.ts.map +1 -0
  25. package/dist/src/review-loop/index.js +7 -0
  26. package/dist/src/review-loop/index.js.map +1 -0
  27. package/dist/src/review-loop/report.d.ts +42 -0
  28. package/dist/src/review-loop/report.d.ts.map +1 -0
  29. package/dist/src/review-loop/report.js +100 -0
  30. package/dist/src/review-loop/report.js.map +1 -0
  31. package/dist/src/review-loop/signal.d.ts +37 -0
  32. package/dist/src/review-loop/signal.d.ts.map +1 -0
  33. package/dist/src/review-loop/signal.js +37 -0
  34. package/dist/src/review-loop/signal.js.map +1 -0
  35. package/dist/src/review-loop/state.d.ts +20 -0
  36. package/dist/src/review-loop/state.d.ts.map +1 -0
  37. package/dist/src/review-loop/state.js +55 -0
  38. package/dist/src/review-loop/state.js.map +1 -0
  39. package/dist/src/review-loop/target.d.ts +23 -0
  40. package/dist/src/review-loop/target.d.ts.map +1 -0
  41. package/dist/src/review-loop/target.js +57 -0
  42. package/dist/src/review-loop/target.js.map +1 -0
  43. package/dist/src/review-loop/threads.d.ts +43 -0
  44. package/dist/src/review-loop/threads.d.ts.map +1 -0
  45. package/dist/src/review-loop/threads.js +40 -0
  46. package/dist/src/review-loop/threads.js.map +1 -0
  47. package/package.json +1 -1
  48. package/plugin/.codex-plugin/plugin.json +2 -2
  49. package/plugin/.mcp.json +1 -1
  50. package/plugin/mcp.json +1 -1
  51. package/plugin/skills/_shared/proactive-routing.md +1 -0
  52. package/plugin/skills/local-pr/SKILL.md +1 -0
  53. package/plugin/skills/review/SKILL.md +44 -21
  54. package/plugin/skills/review-loop/CONTRACT.md +90 -0
  55. package/plugin/skills/review-loop/SKILL.md +813 -0
  56. package/plugin/skills/review-reply/SKILL.md +2 -2
  57. package/plugin/skills/ship/SKILL.md +2 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../../../src/review-loop/signal.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,oDAAoD;IACpD,IAAI,EAAE,OAAO,CAAC;IACd,kDAAkD;IAClD,QAAQ,EAAE,OAAO,CAAC;IAClB,0BAA0B;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,iCAAiC;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,gCAAgC;IAChC,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,oBAAoB,GAAG,OAAO,GAAG,cAAc,GAAG,SAAS,CAAC;AAEhG;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,GAAG,UAAU,CAQzD;AAED;;;;;GAKG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAMpD,CAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 네 값에서 다음에 할 일을 정한다.
3
+ *
4
+ * 이 도출이 문서에 없던 자리다. 감시 모드를 미루며 폴링 스크립트를 지울 때 규칙이
5
+ * 그 안에 있다가 함께 사라졌다. 재리뷰 판정 표는 만들어지지 않는 이름으로 분기하고
6
+ * 있었다. 여기 두면 네 값의 곱집합이 전부 어느 한 쪽으로 간다.
7
+ *
8
+ * 순서가 규칙의 일부다. 닫힌 PR 은 나머지를 볼 것도 없다. 작성자가 명시적으로 다시
9
+ * 봐달라고 눌렀으면 미대응이 남아 있어도 그 뜻을 따른다.
10
+ */
11
+ export function deriveSignal(state) {
12
+ if (!state.open)
13
+ return 'CLOSED';
14
+ if (state.rerequested)
15
+ return 'REREVIEW_REQUESTED';
16
+ if (state.pending > 0)
17
+ return 'WAITING';
18
+ // 아직 한 번도 안 본 PR 은 볼 때다. 이걸 안 가르면 첫 라운드가 "답만 오고 코드는
19
+ // 그대로" 로 읽혀 사람에게 넘어간다 — 실제 PR 에 돌려보고 나온 자리다
20
+ if (!state.reviewed)
21
+ return 'READY';
22
+ return state.changed ? 'READY' : 'REPLIES_ONLY';
23
+ }
24
+ /**
25
+ * 신호마다 루프가 다음에 하는 일.
26
+ *
27
+ * 문서의 재리뷰 판정 표가 이 문자열을 그대로 싣는다. 테스트가 표 행과 이 값을 대조하므로
28
+ * 한쪽만 고치면 게이트에서 걸린다.
29
+ */
30
+ export const SIGNAL_ACTION = {
31
+ CLOSED: '루프를 끝낸다',
32
+ REREVIEW_REQUESTED: '재리뷰한다 — 미대응이 남아 있어도 간다',
33
+ READY: '재리뷰한다',
34
+ REPLIES_ONLY: '재리뷰하지 않고 사람에게 넘긴다',
35
+ WAITING: '지금 상태를 알리고 끝낸다. 다음에 부르면 이어서 돈다',
36
+ };
37
+ //# sourceMappingURL=signal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signal.js","sourceRoot":"","sources":["../../../src/review-loop/signal.ts"],"names":[],"mappings":"AAoBA;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAAC,KAAgB;IAC3C,IAAI,CAAC,KAAK,CAAC,IAAI;QAAE,OAAO,QAAQ,CAAC;IACjC,IAAI,KAAK,CAAC,WAAW;QAAE,OAAO,oBAAoB,CAAC;IACnD,IAAI,KAAK,CAAC,OAAO,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IACxC,oDAAoD;IACpD,4CAA4C;IAC5C,IAAI,CAAC,KAAK,CAAC,QAAQ;QAAE,OAAO,OAAO,CAAC;IACpC,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,cAAc,CAAC;AAClD,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAA+B;IACvD,MAAM,EAAE,SAAS;IACjB,kBAAkB,EAAE,wBAAwB;IAC5C,KAAK,EAAE,OAAO;IACd,YAAY,EAAE,mBAAmB;IACjC,OAAO,EAAE,gCAAgC;CAC1C,CAAC"}
@@ -0,0 +1,20 @@
1
+ export declare const LOGIN_FILE = "my-login";
2
+ export declare const REVIEWED_HEAD_FILE = "reviewed-head";
3
+ /**
4
+ * 라운드 상태를 두는 자리.
5
+ *
6
+ * git 디렉토리 아래다 — git 이 절대 추적하지 않는 자리다. 경로를 절대화해서 어느
7
+ * 디렉토리에서 불러도 같은 곳을 가리킨다 (`state.test.ts` 가 그걸 지킨다). 워킹트리 안에 두면 이 스킬이 플러그인으로 배포돼 도는 남의 레포에서
8
+ * 무시되지 않아 리뷰 대상 diff 에 상태 파일이 섞인다.
9
+ *
10
+ * `--git-common-dir` 이라 워크트리 여럿이 같은 자리를 공유하지만 PR 번호가 하위
11
+ * 디렉토리로 갈라 안 겹친다.
12
+ */
13
+ export declare function stateDir(target: {
14
+ owner: string;
15
+ repo: string;
16
+ prNumber: number;
17
+ }, cwd?: string): string;
18
+ /** 상태 자리의 뿌리. PR 을 아직 못 가린 단계가 대상 문자열을 여기 둔다 */
19
+ export declare function stateRoot(cwd?: string): string;
20
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/review-loop/state.ts"],"names":[],"mappings":"AAeA,eAAO,MAAM,UAAU,aAAa,CAAC;AACrC,eAAO,MAAM,kBAAkB,kBAAkB,CAAC;AAElD;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CACtB,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EACzD,GAAG,SAAgB,GAClB,MAAM,CAoBR;AAED,gDAAgD;AAChD,wBAAgB,SAAS,CAAC,GAAG,SAAgB,GAAG,MAAM,CAMrD"}
@@ -0,0 +1,55 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { resolve } from 'node:path';
3
+ /**
4
+ * 코드가 읽는 파일 이름.
5
+ *
6
+ * 나머지 상태 파일(`round`, `verdicts`, `target` 등)은 절차가 셸로만 다루므로 여기 안
7
+ * 둔다 — 쓰는 쪽이 없는 상수를 늘리면 고쳐도 아무것도 안 따라온다. 문서의 상태 자리
8
+ * 표가 전체 목록이고 `review-verdict-gate.test.ts` 가 그 표와 절차를 대조한다.
9
+ *
10
+ * 스레드 스냅샷은 여기 없다. 조회와 집계가 한 프로세스 안에서 끝나므로 중간 파일이
11
+ * 생기지 않는다.
12
+ */
13
+ const ROOT_DIR = 'gestalt-review-loop';
14
+ export const LOGIN_FILE = 'my-login';
15
+ export const REVIEWED_HEAD_FILE = 'reviewed-head';
16
+ /**
17
+ * 라운드 상태를 두는 자리.
18
+ *
19
+ * git 디렉토리 아래다 — git 이 절대 추적하지 않는 자리다. 경로를 절대화해서 어느
20
+ * 디렉토리에서 불러도 같은 곳을 가리킨다 (`state.test.ts` 가 그걸 지킨다). 워킹트리 안에 두면 이 스킬이 플러그인으로 배포돼 도는 남의 레포에서
21
+ * 무시되지 않아 리뷰 대상 diff 에 상태 파일이 섞인다.
22
+ *
23
+ * `--git-common-dir` 이라 워크트리 여럿이 같은 자리를 공유하지만 PR 번호가 하위
24
+ * 디렉토리로 갈라 안 겹친다.
25
+ */
26
+ export function stateDir(target, cwd = process.cwd()) {
27
+ // 안전 정수 밖의 값은 자릿수가 뭉개져 이름이 지수 표기가 된다. 그러면 서로 다른 PR 이
28
+ // 같은 자리를 공유한다
29
+ if (!Number.isSafeInteger(target.prNumber) || target.prNumber <= 0) {
30
+ throw new Error(`PR 번호가 양의 정수가 아니다: ${target.prNumber}`);
31
+ }
32
+ // 레포까지 넣는다. 번호만으로 가르면 남의 레포 PR 을 볼 때 이쪽 같은 번호와 자리를
33
+ // 나눠 쓴다. 그 reviewed-head 로 새 커밋이 왔는지 보는 순간 재리뷰 판정이 뒤집힌다
34
+ for (const part of [target.owner, target.repo]) {
35
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(part)) {
36
+ throw new Error(`레포를 경로에 못 쓴다: ${JSON.stringify(part)}`);
37
+ }
38
+ }
39
+ // 레포 루트에서 부르면 `.git` 이라는 상대 경로가 온다. cwd 가 바뀐 단계에서 다른
40
+ // 자리를 가리키므로 여기서 절대 경로로 굳힌다 (--path-format=absolute 는 git 2.31+).
41
+ const common = execFileSync('git', ['rev-parse', '--git-common-dir'], {
42
+ cwd,
43
+ encoding: 'utf-8',
44
+ }).trim();
45
+ return resolve(cwd, common, ROOT_DIR, `${target.owner}--${target.repo}--${target.prNumber}`);
46
+ }
47
+ /** 상태 자리의 뿌리. PR 을 아직 못 가린 단계가 대상 문자열을 여기 둔다 */
48
+ export function stateRoot(cwd = process.cwd()) {
49
+ const common = execFileSync('git', ['rev-parse', '--git-common-dir'], {
50
+ cwd,
51
+ encoding: 'utf-8',
52
+ }).trim();
53
+ return resolve(cwd, common, ROOT_DIR);
54
+ }
55
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../../../src/review-loop/state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC;;;;;;;;;GASG;AACH,MAAM,QAAQ,GAAG,qBAAqB,CAAC;AAEvC,MAAM,CAAC,MAAM,UAAU,GAAG,UAAU,CAAC;AACrC,MAAM,CAAC,MAAM,kBAAkB,GAAG,eAAe,CAAC;AAElD;;;;;;;;;GASG;AACH,MAAM,UAAU,QAAQ,CACtB,MAAyD,EACzD,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE;IAEnB,qDAAqD;IACrD,cAAc;IACd,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,QAAQ,IAAI,CAAC,EAAE,CAAC;QACnE,MAAM,IAAI,KAAK,CAAC,sBAAsB,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC3D,CAAC;IACD,mDAAmD;IACnD,wDAAwD;IACxD,KAAK,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/C,IAAI,CAAC,mCAAmC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACpD,MAAM,IAAI,KAAK,CAAC,iBAAiB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC;IACD,qDAAqD;IACrD,iEAAiE;IACjE,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,kBAAkB,CAAC,EAAE;QACpE,GAAG;QACH,QAAQ,EAAE,OAAO;KAClB,CAAC,CAAC,IAAI,EAAE,CAAC;IACV,OAAO,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,KAAK,KAAK,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;AAC/F,CAAC;AAED,gDAAgD;AAChD,MAAM,UAAU,SAAS,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE;IAC3C,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,kBAAkB,CAAC,EAAE;QACpE,GAAG;QACH,QAAQ,EAAE,OAAO;KAClB,CAAC,CAAC,IAAI,EAAE,CAAC;IACV,OAAO,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;AACxC,CAAC"}
@@ -0,0 +1,23 @@
1
+ /** 리뷰 대상에서 읽어낸 것. URL 로 주면 레포까지 함께 온다 */
2
+ export interface PrTarget {
3
+ prNumber: number;
4
+ /** URL 로 준 경우에만 있다. 숫자나 #숫자 로 주면 현재 레포를 쓴다 */
5
+ owner?: string;
6
+ repo?: string;
7
+ }
8
+ /**
9
+ * 사용자가 준 리뷰 대상을 읽는다.
10
+ *
11
+ * 이 값은 뒤에서 상태 디렉토리 이름과 `gh` 인자가 된다. 문서가 이 파싱을 셸로 적던 때는
12
+ * 대상 문자열을 작은따옴표 안에 합성했다. 그래서 따옴표가 섞인 값이 정수 검증에 닿기
13
+ * 전에 명령으로 실행됐다. 문자열을 셸에 넘기지 않으면 그 자리가 없다.
14
+ *
15
+ * **URL 이면 레포도 함께 읽는다.** 번호만 뽑고 버리면 남의 레포 PR 을 가리켜도 조회는
16
+ * 현재 레포의 같은 번호를 본다. 그 수로 승인이 나간다.
17
+ *
18
+ * 받는 꼴은 셋이다 — `18`, `#18`, `https://github.com/owner/repo/pull/18`.
19
+ */
20
+ export declare function parseTarget(target: string): PrTarget;
21
+ /** 번호만 필요한 자리. 레포를 버리므로 대조가 필요한 곳에서는 parseTarget 을 쓴다 */
22
+ export declare function parsePrNumber(target: string): number;
23
+ //# sourceMappingURL=target.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"target.d.ts","sourceRoot":"","sources":["../../../src/review-loop/target.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,8CAA8C;IAC9C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,CAyBpD;AAED,yDAAyD;AACzD,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEpD"}
@@ -0,0 +1,57 @@
1
+ const PR_PATH = /^\/([^/]+)\/([^/]+)\/pull\/(\d+)(?:\/.*)?$/;
2
+ const GITHUB_HOSTS = new Set(['github.com', 'www.github.com']);
3
+ const OWNER_REPO = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
4
+ /**
5
+ * 사용자가 준 리뷰 대상을 읽는다.
6
+ *
7
+ * 이 값은 뒤에서 상태 디렉토리 이름과 `gh` 인자가 된다. 문서가 이 파싱을 셸로 적던 때는
8
+ * 대상 문자열을 작은따옴표 안에 합성했다. 그래서 따옴표가 섞인 값이 정수 검증에 닿기
9
+ * 전에 명령으로 실행됐다. 문자열을 셸에 넘기지 않으면 그 자리가 없다.
10
+ *
11
+ * **URL 이면 레포도 함께 읽는다.** 번호만 뽑고 버리면 남의 레포 PR 을 가리켜도 조회는
12
+ * 현재 레포의 같은 번호를 본다. 그 수로 승인이 나간다.
13
+ *
14
+ * 받는 꼴은 셋이다 — `18`, `#18`, `https://github.com/owner/repo/pull/18`.
15
+ */
16
+ export function parseTarget(target) {
17
+ const trimmed = target.trim();
18
+ if (/^https?:\/\//i.test(trimmed)) {
19
+ let url;
20
+ try {
21
+ url = new URL(trimmed);
22
+ }
23
+ catch {
24
+ throw new Error(`PR URL 로 못 읽었습니다: ${target}`);
25
+ }
26
+ // 호스트를 먼저 본다. 안 보면 아무 도메인의 /pull/ 경로가 PR 번호로 읽혀
27
+ // 조회 대상이 사용자가 지목한 것과 조용히 갈린다
28
+ if (!GITHUB_HOSTS.has(url.hostname.toLowerCase())) {
29
+ throw new Error(`github.com 주소가 아닙니다: ${target}`);
30
+ }
31
+ const m = PR_PATH.exec(url.pathname);
32
+ if (!m)
33
+ throw new Error(`PR URL 로 못 읽었습니다: ${target}`);
34
+ const [, owner, repo, num] = m;
35
+ if (!OWNER_REPO.test(owner) || !OWNER_REPO.test(repo)) {
36
+ throw new Error(`레포를 못 읽었습니다: ${target}`);
37
+ }
38
+ return { prNumber: toPrNumber(num, target), owner: owner, repo: repo };
39
+ }
40
+ return { prNumber: toPrNumber(trimmed.replace(/^#/, ''), target) };
41
+ }
42
+ /** 번호만 필요한 자리. 레포를 버리므로 대조가 필요한 곳에서는 parseTarget 을 쓴다 */
43
+ export function parsePrNumber(target) {
44
+ return parseTarget(target).prNumber;
45
+ }
46
+ function toPrNumber(raw, target) {
47
+ if (!/^\d+$/.test(raw)) {
48
+ throw new Error(`PR 번호로 못 읽었습니다: ${target}`);
49
+ }
50
+ const n = Number(raw);
51
+ // 자릿수가 많으면 Number 가 지수 표기로 뭉개 정수 검사를 통과한다
52
+ if (!Number.isSafeInteger(n) || n <= 0) {
53
+ throw new Error(`PR 번호로 못 읽었습니다: ${target}`);
54
+ }
55
+ return n;
56
+ }
57
+ //# sourceMappingURL=target.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"target.js","sourceRoot":"","sources":["../../../src/review-loop/target.ts"],"names":[],"mappings":"AAQA,MAAM,OAAO,GAAG,4CAA4C,CAAC;AAC7D,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE,gBAAgB,CAAC,CAAC,CAAC;AAC/D,MAAM,UAAU,GAAG,mCAAmC,CAAC;AAEvD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,WAAW,CAAC,MAAc;IACxC,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IAE9B,IAAI,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAClC,IAAI,GAAQ,CAAC;QACb,IAAI,CAAC;YACH,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;QACzB,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,KAAK,CAAC,qBAAqB,MAAM,EAAE,CAAC,CAAC;QACjD,CAAC;QACD,gDAAgD;QAChD,6BAA6B;QAC7B,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YAClD,MAAM,IAAI,KAAK,CAAC,wBAAwB,MAAM,EAAE,CAAC,CAAC;QACpD,CAAC;QACD,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACrC,IAAI,CAAC,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,MAAM,EAAE,CAAC,CAAC;QACvD,MAAM,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,KAAM,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAK,CAAC,EAAE,CAAC;YACxD,MAAM,IAAI,KAAK,CAAC,gBAAgB,MAAM,EAAE,CAAC,CAAC;QAC5C,CAAC;QACD,OAAO,EAAE,QAAQ,EAAE,UAAU,CAAC,GAAI,EAAE,MAAM,CAAC,EAAE,KAAK,EAAE,KAAM,EAAE,IAAI,EAAE,IAAK,EAAE,CAAC;IAC5E,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,CAAC;AACrE,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,aAAa,CAAC,MAAc;IAC1C,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC;AACtC,CAAC;AAED,SAAS,UAAU,CAAC,GAAW,EAAE,MAAc;IAC7C,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,mBAAmB,MAAM,EAAE,CAAC,CAAC;IAC/C,CAAC;IACD,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IACtB,0CAA0C;IAC1C,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,mBAAmB,MAAM,EAAE,CAAC,CAAC;IAC/C,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC"}
@@ -0,0 +1,43 @@
1
+ /** 코멘트 하나. 삭제된 계정이면 author 가 null 로 온다 */
2
+ interface Comment {
3
+ author: {
4
+ login: string;
5
+ } | null;
6
+ }
7
+ /**
8
+ * GraphQL `reviewThreads` 가 돌려주는 스레드에서 판정에 쓰는 부분만.
9
+ *
10
+ * 개설자와 마지막 코멘트를 별칭으로 따로 받는다. 코멘트를 한 덩어리로 받으면 긴
11
+ * 스레드에서 잘려, 마지막이라고 읽은 게 실제 마지막이 아니게 된다 — 그 값이
12
+ * "작성자가 답했는지"를 정하므로 판정이 양방향으로 뒤집힌다.
13
+ */
14
+ export interface ReviewThread {
15
+ isResolved: boolean;
16
+ isOutdated: boolean;
17
+ opener: {
18
+ nodes: Comment[];
19
+ };
20
+ latest: {
21
+ nodes: Comment[];
22
+ };
23
+ }
24
+ /**
25
+ * 내가 열어둔 스레드 중 작성자가 아직 손대지 않은 수.
26
+ *
27
+ * 이 수가 0이어야 승인이 나간다. 그래서 셀 수 없는 상태는 0이 아니라 예외로 답한다 —
28
+ * 셸 스크립트로 적던 때는 로그인이 빈 문자열이면 어떤 스레드와도 안 맞아 0과 종료
29
+ * 코드 0이 함께 나왔다. 그 값이 승인 게이트를 그대로 통과했다.
30
+ *
31
+ * 스레드가 닫혔거나, 코드가 바뀌어 outdated 가 됐거나, 마지막 코멘트가 내 것이 아니면
32
+ * 대응된 것으로 센다 — 마지막은 작성자가 답을 달았다는 뜻이다.
33
+ */
34
+ export declare function countPending(threads: readonly ReviewThread[], me: string): number;
35
+ /**
36
+ * 내가 연 스레드인지.
37
+ *
38
+ * 남이 연 스레드는 이 루프가 판정할 자리가 아니다 — 그건 그 사람이 닫는다. 사람에게
39
+ * 보이는 스레드 수도 이 필터를 거친 것이라야 한다.
40
+ */
41
+ export declare function isMine(thread: ReviewThread, me: string): boolean;
42
+ export {};
43
+ //# sourceMappingURL=threads.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"threads.d.ts","sourceRoot":"","sources":["../../../src/review-loop/threads.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAC1C,UAAU,OAAO;IACf,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAClC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE;QAAE,KAAK,EAAE,OAAO,EAAE,CAAA;KAAE,CAAC;IAC7B,MAAM,EAAE;QAAE,KAAK,EAAE,OAAO,EAAE,CAAA;KAAE,CAAC;CAC9B;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAQjF;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAEhE"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * 내가 열어둔 스레드 중 작성자가 아직 손대지 않은 수.
3
+ *
4
+ * 이 수가 0이어야 승인이 나간다. 그래서 셀 수 없는 상태는 0이 아니라 예외로 답한다 —
5
+ * 셸 스크립트로 적던 때는 로그인이 빈 문자열이면 어떤 스레드와도 안 맞아 0과 종료
6
+ * 코드 0이 함께 나왔다. 그 값이 승인 게이트를 그대로 통과했다.
7
+ *
8
+ * 스레드가 닫혔거나, 코드가 바뀌어 outdated 가 됐거나, 마지막 코멘트가 내 것이 아니면
9
+ * 대응된 것으로 센다 — 마지막은 작성자가 답을 달았다는 뜻이다.
10
+ */
11
+ export function countPending(threads, me) {
12
+ const login = me.trim().toLowerCase();
13
+ if (!login) {
14
+ throw new Error('내 로그인이 비었다 — 집계하지 않는다');
15
+ }
16
+ // GitHub 로그인은 대소문자를 안 가린다. 그대로 비교하면 캐시에 적힌 철자가 API 응답과
17
+ // 한 글자만 달라도 어떤 스레드와도 안 맞아 미대응이 0이 되고 승인이 열린다
18
+ return threads.filter((t) => isPending(t, login)).length;
19
+ }
20
+ /**
21
+ * 내가 연 스레드인지.
22
+ *
23
+ * 남이 연 스레드는 이 루프가 판정할 자리가 아니다 — 그건 그 사람이 닫는다. 사람에게
24
+ * 보이는 스레드 수도 이 필터를 거친 것이라야 한다.
25
+ */
26
+ export function isMine(thread, me) {
27
+ return thread.opener.nodes[0]?.author?.login?.toLowerCase() === me.trim().toLowerCase();
28
+ }
29
+ /** me 는 소문자로 정규화된 로그인이다 */
30
+ function isPending(thread, me) {
31
+ if (thread.isResolved)
32
+ return false;
33
+ if (thread.isOutdated)
34
+ return false;
35
+ if (!isMine(thread, me))
36
+ return false;
37
+ const last = thread.latest.nodes[0]?.author?.login?.toLowerCase();
38
+ return last === me;
39
+ }
40
+ //# sourceMappingURL=threads.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"threads.js","sourceRoot":"","sources":["../../../src/review-loop/threads.ts"],"names":[],"mappings":"AAmBA;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAAC,OAAgC,EAAE,EAAU;IACvE,MAAM,KAAK,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACtC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;IAC3C,CAAC;IACD,uDAAuD;IACvD,6CAA6C;IAC7C,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC;AAC3D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,MAAM,CAAC,MAAoB,EAAE,EAAU;IACrD,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AAC1F,CAAC;AAED,2BAA2B;AAC3B,SAAS,SAAS,CAAC,MAAoB,EAAE,EAAU;IACjD,IAAI,MAAM,CAAC,UAAU;QAAE,OAAO,KAAK,CAAC;IACpC,IAAI,MAAM,CAAC,UAAU;QAAE,OAAO,KAAK,CAAC;IACpC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;QAAE,OAAO,KAAK,CAAC;IAEtC,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IAClE,OAAO,IAAI,KAAK,EAAE,CAAC;AACrB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tienne/gestalt",
3
- "version": "0.75.1",
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.75.1",
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"
@@ -22,7 +22,7 @@
22
22
  "interface": {
23
23
  "displayName": "Gestalt",
24
24
  "shortDescription": "Requirements-to-spec pipeline with review and PR workflows",
25
- "longDescription": "Gestalt turns vague ideas into validated specifications through Gestalt-principle interviews, then drives execution, code review, and PR authoring. Bundles 19 workflow skills and 27 role agents over 12 MCP tools.",
25
+ "longDescription": "Gestalt turns vague ideas into validated specifications through Gestalt-principle interviews, then drives execution, code review, and PR authoring. Bundles 21 workflow skills and 27 role agents over 12 MCP tools.",
26
26
  "developerName": "tienne",
27
27
  "category": "Developer Tools",
28
28
  "capabilities": [
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.75.1", "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.75.1", "serve"],
5
+ "args": ["-y", "@tienne/gestalt@0.76.1", "serve"],
6
6
  "startup_timeout_sec": 180,
7
7
  "tool_timeout_sec": 900
8
8
  }
@@ -41,6 +41,7 @@
41
41
  | PR·브랜치·커밋 코드 리뷰 요청 | `/review` 스킬 사용 |
42
42
  | 받은 리뷰 반영·답글 게시 요청 ("리뷰 반영해줘", "리뷰 코멘트에 답해줘", "받은 리뷰 처리해줘") | `review-reply` 스킬 사용 (스레드 수집 → 유형 분류 승인 → 수정·커밋 → 답글 승인 → 게시) |
43
43
  | 리뷰가 수렴할 때까지 돌려 GitHub까지 내보내는 요청 ("출하해줘", "리뷰 통과할 때까지", "코파일럿 리뷰까지 받아줘") | `ship` 스킬 사용 (로컬 PR → 리뷰 수렴 루프 → 승인 → draft PR → Copilot 수렴 루프 → ready). 리뷰 한 번만이면 `review` |
44
+ | 남의 PR을 리뷰하고 대응까지 따라가는 요청 ("리뷰하고 대응 확인해줘", "대응했으면 재리뷰", "approve 날 때까지") | `review-loop` 스킬 사용 (리뷰 → 인라인 코멘트 → 판정 게시 → 대응 확인 → 재리뷰). **한 번 부르면 한 번 본다** — 이어 보려면 사람이 다시 부른다. 리뷰 한 번만이면 `review`, 내 PR을 밀어 올리는 쪽은 `ship` |
44
45
  | PR 작성·생성 요청 ("PR 만들어줘", "PR 작성해줘", "PR 올려줘") | `gestalt:pr` 스킬 사용 |
45
46
  | 실행 태스크를 외부 런타임 워커로 뿌리는 요청 ("orca로 실행", "codex로 실행", "워커 띄워서 실행") | `dispatch` 스킬 사용 (런타임 감지 → 같은 워크트리에 터미널 → worker_done 대기 → ready 재계산). 런타임 없으면 execute의 기본 병렬 경로 |
46
47
 
@@ -48,6 +48,7 @@ outputs:
48
48
  | 이미 있는 변경을 검토받기 | `review` |
49
49
  | 받은 리뷰에 답하기 | `review-reply` |
50
50
  | 리뷰가 수렴할 때까지 돌려 GitHub까지 내보내기 | `ship` |
51
+ | 남의 GitHub PR을 리뷰하고 대응을 지켜보다 재리뷰하기 | `review-loop` |
51
52
 
52
53
  원격 PR은 사람이 읽고 판단하라고 올린다. 에이전트끼리 주고받는 데는 `gh`도 인증도 원격 왕복도 군더더기다. 워크트리 여럿이 `.gestalt/reviews.db` 하나를 공유하므로 어느 워크트리에서 쳐도 같은 목록을 본다.
53
54
 
@@ -34,6 +34,10 @@ inputs:
34
34
  type: string
35
35
  required: false
36
36
  description: "인라인 코멘트를 누가 읽는지. peer | junior. 사용자가 붙인 `--audience junior`나 `--junior` 플래그가 이 값으로 들어온다 (`--junior` 축약은 이 스킬 전용이다 — 받는 값이 둘뿐이라 축약이 성립한다). 기본값 peer — 지금까지의 코멘트가 그대로 나온다"
37
+ postVerdict:
38
+ type: boolean
39
+ required: false
40
+ description: "GitHub PR에 리뷰 이벤트(APPROVE, REQUEST_CHANGES, COMMENT)까지 남길지. 기본값 true. false면 인라인 코멘트만 올리고 이벤트는 `COMMENT`로 고정한다 — PR의 리뷰 상태를 안 건드린다. 라운드를 도는 스킬이 판정을 자기가 내려고 이 스킬을 부를 때 쓴다. `prTarget`이 `local`이면 이 값을 안 본다"
37
41
  outputs:
38
42
  - reviewIntent
39
43
  - changeContext
@@ -41,6 +45,7 @@ outputs:
41
45
  - verdict
42
46
  - continuityVerdict
43
47
  - postedReview
48
+ - reviewSummary
44
49
  ---
45
50
 
46
51
  # Review Skill
@@ -70,7 +75,7 @@ execute 세션 없이 PR, 브랜치, 커밋의 변경사항을 직접 리뷰 파
70
75
  /review --junior # 인라인 코멘트를 주니어 눈높이로 (= --audience junior)
71
76
  ```
72
77
 
73
- 리뷰 한 번이 이 스킬의 범위입니다. 이슈가 없어질 때까지 리뷰와 대응을 반복하고 GitHub PR까지 내보내려면 `ship` 스킬을 씁니다 그쪽이 라운드마다 이 스킬을 부릅니다.
78
+ 리뷰 한 번이 이 스킬의 범위입니다. 반복은 부르는 쪽이 갈립니다. **내 PR**을 리뷰 통과 상태까지 밀어 GitHub 내보내려면 `ship`입니다. **남의 PR**을 리뷰하고 대응을 지켜보다 재리뷰하려면 `review-loop`이고요. 둘 다 라운드마다 이 스킬을 부릅니다.
74
79
 
75
80
  ## 전제 조건
76
81
 
@@ -579,9 +584,11 @@ Agent {
579
584
 
580
585
  #### 진입 경로 두 가지
581
586
 
582
- 이 단계는 `/review`를 처음부터 돌린 흐름뿐 아니라, **대화 도중 "이제 PR에 코멘트 남겨줘"처럼 게시만 따로 요청**받았을 때도 진입점이 됩니다 (위 triggers의 "PR에 코멘트 남겨줘" 등). 두 경우 모두 아래 **신선도 가드를 먼저 통과해야** 게시할 수 있습니다.
587
+ 이 단계는 `/review`를 처음부터 돌린 흐름뿐 아니라, **대화 도중 "이제 PR에 코멘트 남겨줘"처럼 게시만 따로 요청**받았을 때도 진입점이 됩니다 (위 triggers의 "PR에 코멘트 남겨줘" 등). 두 경우 모두 아래 **consensus 일치 검사를 먼저 통과해야** 게시할 수 있습니다.
588
+
589
+ **`prTarget`이 `none`이면 여기서 끝냅니다.** 브랜치나 커밋 범위를 리뷰한 경우가 그런데, GitHub에도 로컬에도 대응하는 PR이 없는 자리입니다. 4.7단계를 통째로 건너뛰고 결과 표시로 갑니다 — 게시할 자리가 없는데 consensus를 다시 맞춰볼 이유가 없습니다.
583
590
 
584
- #### 신선도 가드 (stale consensus 게시 금지)
591
+ #### consensus 일치 검사 (stale이면 게시 금지)
585
592
 
586
593
  게시 직전에, 게시하려는 consensus가 **현재 diff와 일치하는지** 반드시 확인합니다. 리뷰를 끝낸 뒤 코드가 바뀌었거나(커밋 추가·로컬 수정), 애초에 활성 리뷰 세션이 없으면 그 consensus는 stale이므로 **그대로 올리지 않습니다.**
587
594
 
@@ -598,12 +605,14 @@ pnpm tsx bin/gestalt.ts pr --json show <id> # headSha 필드로 비교
598
605
 
599
606
  판단 기준:
600
607
 
601
- - **이번 세션에 방금 리뷰를 끝냈고 그 뒤 diff 변화가 없다** → consensus 신선함. 곧장 게시 진행.
602
- - **리뷰 후 코드가 바뀌었다 / 활성 리뷰 세션이 없다 / 다른 세션의 오래된 결과다** → 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을 진행합니다. 사용자에게 "변경이 있어 현재 코드로 다시 리뷰한 뒤 게시할게요"라고 한 줄 알립니다.
603
610
 
604
611
  인라인 코멘트는 **언제 요청받든 항상 "현재 diff 기준 consensus + code-review-writer voice"** 로만 게시됩니다. 옛 리뷰 메모리를 그대로 옮겨 적거나 Claude가 손으로 코멘트를 짜는 경로는 없습니다.
605
612
 
606
- **PR 식별.** 대상 판별은 1단계 직후에 이미 끝났습니다. 여기서는 그때 보관한 `prTarget`과 PR 식별자를 그대로 씁니다. **같은 조회를 다시 하지 않습니다.** `prTarget`이 `none`이면(GitHub에도 로컬에도 대응하는 PR 없는 브랜치나 커밋 범위 리뷰) 이 단계를 통째로 건너뛰고 결과 표시로 갑니다.
613
+ #### 게시 준비 (PR 식별, audience 확인, 게시 확인)
614
+
615
+ **PR 식별.** 대상 판별은 1단계 직후에 이미 끝났습니다. 여기서는 그때 보관한 `prTarget`과 PR 식별자를 그대로 씁니다. **같은 조회를 다시 하지 않습니다.**
607
616
 
608
617
  게시 직전에 그 PR이 아직 살아 있는지만 한 번 확인합니다.
609
618
 
@@ -621,7 +630,9 @@ pnpm tsx bin/gestalt.ts pr --json show <id> 2>/dev/null
621
630
 
622
631
  **게시 확인.** PR이 식별되면 사용자에게 한 번 확인합니다: **"발견된 이슈 N건을 PR #<number 또는 로컬 PR id>에 인라인 코멘트로 게시할까요?"** 동의하지 않으면 리포트만 보여주고 종료합니다.
623
632
 
624
- **코멘트 본문 작성 (code-review-writer).** **서브에이전트에 위임합니다.** 이 에이전트는 본문 18.8KB에 `author-voice.md` 19KB를 딸고 오는, 이 스킬에서 제일 무거운 자리입니다.
633
+ #### 코멘트 본문 작성 (code-review-writer)
634
+
635
+ **서브에이전트에 위임합니다.** 이 에이전트는 본문 18.8KB에 `author-voice.md` 19KB를 딸고 오는, 이 스킬에서 제일 무거운 자리입니다.
625
636
 
626
637
  ```
627
638
  Agent {
@@ -654,7 +665,22 @@ Agent {
654
665
  }
655
666
  ```
656
667
 
657
- **어투 검사 (필수).** 작성된 코멘트를 게시 전에 스캔합니다. 에이전트가 룰북을 내장하고 자가점검도 하지만 **리뷰 대상 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에 있던 말이 그대로 딸려오는 자리**는 자가점검으로 안 걸립니다 — 원문에 있으니 맞는 말이라고 판단하는 자리라서요. 그 자리를 잡는 게 이 검사입니다.
658
684
 
659
685
  **코멘트마다 파일 하나로 떨굽니다. 한 파일에 모으지 않습니다.** 모아서 한 번에 스캔하면 검사가 배치 전체를 한 덩어리로 봅니다. 그러면 코멘트 하나를 통째로 `>` 인용으로 감싸도 다른 코멘트의 산문에 묻혀 안 걸립니다. 코멘트별로 갈라야 그 판정이 코멘트 단위로 섭니다. 어느 코멘트가 걸렸는지도 파일 이름으로 바로 읽히고요.
660
686
 
@@ -707,20 +733,9 @@ done
707
733
 
708
734
  **큰따옴표로 감싼 평문은 형태로 못 가릅니다.** 한국어에서 큰따옴표는 인용만이 아니라 강조로도 쓰여서요. 룰북 Do-NOT 목록이 "큰따옴표 안 직접 인용"을 예외로 두지만 그건 사람과 모델이 판단하는 자리의 기준입니다. 자동 스캔은 위 셋만 뺍니다. 원문을 가리켜야 하면 백틱이나 블록인용을 씁니다.
709
735
 
710
- **`path`·`line`·`side`·`severity`는 메인 세션이 채웁니다.** 서브에이전트는 `id`와 본문만 돌려주고 메인이 `id`로 `mergedIssues`를 되짚어 나머지를 붙입니다. 전부 코멘트 문체와 무관한 기계적 매핑이라 위임할 이유가 없고 서브에이전트가 라인이나 등급을 바꿔 적을 여지도 없앱니다. **원본을 이미 들고 있는 값을 되돌려 받아 쓰지 않습니다.**
736
+ #### 리뷰 이벤트 결정 (postVerdict와 본인 PR 예외)
711
737
 
712
- - `side`는 diff의 신규 라인이면 `RIGHT`, 삭제된 라인을 짚으면 `LEFT`입니다.
713
- - 라인 매핑이 불확실한 이슈(파일 전반이거나 구조적인 것)는 `comments`에 넣지 않고 리뷰 `body` 요약에 한 줄로 돌립니다. 임의 라인에 억지로 붙이지 않습니다.
714
-
715
- 아래 규칙은 `code-review-writer` AGENT.md에 있어서 서브에이전트가 읽습니다. 여기 적어두는 건 사람이 읽을 계약이고 두 곳이 갈라지면 AGENT.md가 기준입니다. (바로 위 `path`·`line`·`side` 규칙은 반대로 **스킬 쪽에만** 있습니다 — 메인 세션이 하는 일이라 AGENT.md에 없습니다.)
716
-
717
- - code-review-writer는 `author-voice.md`(제안형·온기·물결·이모지)와 `ai-tell-quick-rules.md`(음차 교정)를 이미 내장하므로 **리포트처럼 humanize-monolith로 통째로 윤문하지는 않습니다.** 대신 위의 `humanize-scan` 검사를 거칩니다 — 내장이 자가점검을 시키는 것이고 검사는 그게 실제로 됐는지 보는 것이라 층이 다릅니다.
718
- - 에이전트 룰에 따라 `[출처]` 태깅, "…권장." 체언 종지는 쓰지 않습니다. 이건 Claude artifact이지 실제 리뷰어 어투가 아닙니다.
719
- - **출처를 밝히는 태그는 형태를 가리지 않고 쓰지 않습니다.** `[게슈탈트 리뷰]`, `[Gestalt]`, `[AI 리뷰]`, 🤖 처럼 도구가 썼다는 표시를 붙이지 않습니다. 리뷰는 계정 주인이 남기는 것입니다. **내부 리뷰 에이전트 이름(QA, Architect, security-reviewer 등)도 본문에 드러내지 않습니다** — 관점이 여럿이어도 코멘트는 리뷰어 한 사람이 남긴 것처럼 씁니다.
720
- - **강제성은 `r:`/`c:`/`a:` 접두어로 표기합니다** (레포에 자체 리뷰 컨벤션이 없을 때의 기본값). 코멘트 본문 맨 앞에 severity에 따라 붙입니다 — `r:` 꼭 반영(critical/high), `c:` 웬만하면 반영(warning), `a:` 사소한 의견(suggestion). 접두어는 강제성 라벨이고 본문 어투는 그대로 제안형입니다. **접두어 앞에는 아무것도 오지 않습니다** — 출처 태그나 굵은 제목 줄이 접두어를 밀어내면 리뷰이가 강제성을 한눈에 못 봅니다. (리뷰 이벤트 판정은 접두어가 아니라 `severity`로 하므로 그쪽은 영향받지 않습니다.)
721
- - **개행은 GitHub 렌더링 기준으로 조립합니다.** GitHub GFM은 한 줄 개행(`\n`)을 무시하고 같은 문단으로 이어 붙이므로, 줄을 실제로 나누려면 **빈 줄(`\n\n`)로 블록을 분리**해야 합니다. 접두어 → 문제 설명 → 제안 → 코드 스니펫을 각각 빈 줄로 띄우고 여러 줄 코드는 fenced code block(` ```lang ``` `)으로 감쌉니다. 한 줄 개행으로 이어 붙이면 PR에서 한 덩어리로 뭉쳐 읽기 어렵습니다 (code-review-writer의 Output Format 개행 규칙과 동일).
722
-
723
- **리뷰 이벤트 결정.** `mergedIssues`의 `severity`로 리뷰 전체의 `event`를 정합니다. 본문 첫 글자를 파싱하지 않습니다 — 접두어는 사람이 읽는 라벨이지 판정 입력이 아닙니다.
738
+ `mergedIssues`의 `severity`로 리뷰 전체의 `event`를 정합니다. 본문 글자를 파싱하지 않습니다 — 접두어는 사람이 읽는 라벨이지 판정 입력이 아닙니다.
724
739
 
725
740
  - `critical`이나 `high`가 하나라도 있으면 → `REQUEST_CHANGES` (본문 접두어 `r:`)
726
741
  - 없고 `warning`만 있으면 → `COMMENT` (접두어 `c:`)
@@ -728,6 +743,14 @@ done
728
743
 
729
744
  4단계 `overallApproved`(결함 심급 blocking 여부)와도 일치합니다 — blocking 이슈가 있으면 critical이나 high가 존재하므로 `REQUEST_CHANGES`가 됩니다. 단 `APPROVE`/`REQUEST_CHANGES`는 리뷰 상태를 바꾸는 행위이므로, 위 **"게시 확인"**에서 사용자 동의를 받은 뒤에만 게시합니다.
730
745
 
746
+ **`postVerdict`가 `false`면 위 계산을 하지 않고 `event=COMMENT`로 고정합니다.** 인라인 코멘트는 그대로 올라가고 PR의 리뷰 상태만 안 건드립니다. 부르는 쪽이 판정을 자기가 내겠다는 뜻이라, 여기서 `APPROVE`나 `REQUEST_CHANGES`를 먼저 내보내면 그쪽 판정이 도착하기 전에 리뷰 상태가 정해집니다. `COMMENT`는 기존 상태를 안 바꾸므로 뒤이어 오는 판정이 그대로 섭니다.
747
+
748
+ 이 값은 `prTarget`이 `github`일 때만 걸립니다. 로컬 PR은 `review_publish`가 파이프라인과 같은 경계로 판정을 정하므로 부르는 쪽이 그걸 억제할 이유가 없습니다.
749
+
750
+ **부르는 쪽이 게시 결과를 확인할 수 있게 합니다.** `postVerdict: false`로 불렀는데 이 스킬이 그 값을 못 읽고 `APPROVE`나 `REQUEST_CHANGES`를 내보냈다면, 그쪽 판정이 도착하기 전에 리뷰 상태가 이미 정해집니다. 게시 직후 `gh pr view <번호> --json reviewDecision`으로 상태를 확인해 `postedReview`에 담아 돌려줍니다 — 부르는 쪽이 그 값으로 어긋남을 알아챕니다.
751
+
752
+ **`reviewSummary`를 출력으로 돌려줍니다.** 위 서브에이전트가 돌려준 `{ comments, summary }`의 `summary`를 그대로 담습니다 — 따로 짓지 않습니다. `postVerdict: false`로 부른 쪽이 자기 판정 본문을 지을 때 이 값을 뼈대로 씁니다 — 같은 에이전트를 판정 본문만으로 한 번 더 부르면 룰북을 라운드마다 두 번 싣게 됩니다.
753
+
731
754
  > **본인 PR 예외 (github)**: GitHub는 PR 작성자 본인이 자기 PR을 `APPROVE`/`REQUEST_CHANGES`하는 걸 막습니다(422). `gh pr view --json author`와 `gh api user`로 작성자가 현재 사용자와 같은지 확인하고 같으면 `event=COMMENT`로 폴백해 게시합니다 (접두어 r/c/a는 본문에 그대로 유지). 이때 사용자에게 "본인 PR이라 승인/변경요청 상태는 못 걸어서 코멘트로 남겼어요"라고 한 줄 알립니다. **local**은 `gestalt pr review`가 이 제약을 두지 않습니다 — author가 본인과 같아도 verdict 그대로 게시하되, 사용자에게 그 사실만 한 줄 알립니다.
732
755
 
733
756
  **게시.** `prTarget`에 따라 갈립니다.