@elyun/bylane 1.3.0 → 1.4.0

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.
@@ -0,0 +1,88 @@
1
+ #!/bin/bash
2
+ # byLane pre-commit 보안 검사
3
+
4
+ RED='\033[0;31m'
5
+ YELLOW='\033[1;33m'
6
+ GREEN='\033[0;32m'
7
+ NC='\033[0m'
8
+
9
+ ERRORS=0
10
+ WARNINGS=0
11
+
12
+ echo "보안 검사 실행 중..."
13
+
14
+ # 스테이징된 파일 목록
15
+ STAGED=$(git diff --cached --name-only --diff-filter=ACMR)
16
+
17
+ if [ -z "$STAGED" ]; then
18
+ echo -e "${GREEN}스테이징된 파일 없음. 건너뜀.${NC}"
19
+ exit 0
20
+ fi
21
+
22
+ # 1. 민감한 파일명 패턴
23
+ SENSITIVE_FILES=$(echo "$STAGED" | grep -iE \
24
+ '\.env$|\.env\.|credentials|secrets|\.pem$|\.key$|\.p12$|\.pfx$|id_rsa|id_dsa|\.secret' \
25
+ || true)
26
+
27
+ if [ -n "$SENSITIVE_FILES" ]; then
28
+ echo -e "${RED}[CRITICAL] 민감한 파일이 커밋에 포함되어 있습니다:${NC}"
29
+ echo "$SENSITIVE_FILES" | sed 's/^/ - /'
30
+ ERRORS=$((ERRORS + 1))
31
+ fi
32
+
33
+ # 2. 하드코딩된 시크릿 패턴
34
+ SECRET_PATTERNS=(
35
+ 'AKIA[0-9A-Z]{16}' # AWS Access Key
36
+ 'sk-[a-zA-Z0-9]{32,}' # OpenAI / Anthropic API Key
37
+ 'ghp_[a-zA-Z0-9]{36}' # GitHub Personal Access Token
38
+ 'ghs_[a-zA-Z0-9]{36}' # GitHub Actions Token
39
+ 'xox[baprs]-[0-9a-zA-Z]{10,}' # Slack Token
40
+ 'AIza[0-9A-Za-z_-]{35}' # Google API Key
41
+ 'ya29\.[0-9A-Za-z_-]+' # Google OAuth Token
42
+ 'Bearer [a-zA-Z0-9._-]{20,}' # Generic Bearer Token (긴 것만)
43
+ 'password[[:space:]]*=[[:space:]]*"[^"]{6,}' # Hardcoded password (double quote)
44
+ "password[[:space:]]*=[[:space:]]*'[^']{6,}" # Hardcoded password (single quote)
45
+ 'api_key[[:space:]]*[=:][[:space:]]*"[^"]{8,}' # Hardcoded API key
46
+ )
47
+
48
+ for pattern in "${SECRET_PATTERNS[@]}"; do
49
+ MATCHES=$(git diff --cached -U0 | grep '^+' | grep -v '^+++' | grep -iE "$pattern" || true)
50
+ if [ -n "$MATCHES" ]; then
51
+ echo -e "${RED}[CRITICAL] 하드코딩된 시크릿 의심 패턴 발견 (${pattern}):${NC}"
52
+ echo "$MATCHES" | head -3 | sed 's/^/ /'
53
+ ERRORS=$((ERRORS + 1))
54
+ fi
55
+ done
56
+
57
+ # 3. console.log (JS/TS 파일)
58
+ JS_FILES=$(echo "$STAGED" | grep -E '\.(js|ts|jsx|tsx)$' || true)
59
+ if [ -n "$JS_FILES" ]; then
60
+ CONSOLE_MATCHES=$(git diff --cached -U0 -- $JS_FILES | grep '^+' | grep -v '^+++' | grep 'console\.log' || true)
61
+ if [ -n "$CONSOLE_MATCHES" ]; then
62
+ echo -e "${YELLOW}[WARN] console.log 발견:${NC}"
63
+ echo "$CONSOLE_MATCHES" | head -3 | sed 's/^/ /'
64
+ WARNINGS=$((WARNINGS + 1))
65
+ fi
66
+ fi
67
+
68
+ # 4. node_modules 실수 커밋
69
+ NODE_MODULES=$(echo "$STAGED" | grep '^node_modules/' || true)
70
+ if [ -n "$NODE_MODULES" ]; then
71
+ echo -e "${RED}[CRITICAL] node_modules가 커밋에 포함되어 있습니다.${NC}"
72
+ ERRORS=$((ERRORS + 1))
73
+ fi
74
+
75
+ # 5. 결과 출력
76
+ echo ""
77
+ if [ $ERRORS -gt 0 ]; then
78
+ echo -e "${RED}보안 검사 실패: $ERRORS 개의 오류가 있습니다.${NC}"
79
+ echo "커밋이 차단되었습니다. 오류를 수정하세요."
80
+ echo "(검사를 건너뛰려면: git commit --no-verify)"
81
+ exit 1
82
+ elif [ $WARNINGS -gt 0 ]; then
83
+ echo -e "${YELLOW}경고: $WARNINGS 개의 주의 사항이 있습니다. 커밋은 계속됩니다.${NC}"
84
+ else
85
+ echo -e "${GREEN}보안 검사 통과.${NC}"
86
+ fi
87
+
88
+ exit 0
@@ -17,7 +17,9 @@ description: byLane 메인 커맨드. 자연어로 전체 개발 워크플로우
17
17
  /bylane commit
18
18
  /bylane pr
19
19
  /bylane review [PR번호]
20
+ /bylane review-loop — 5분 주기 자동 리뷰 루프 시작
20
21
  /bylane respond [PR번호]
22
+ /bylane respond-loop — 5분 주기 자동 대응 루프 시작
21
23
  /bylane notify
22
24
  /bylane status — 현재 상태 한 줄 요약
23
25
  ```
@@ -37,7 +39,9 @@ description: byLane 메인 커맨드. 자연어로 전체 개발 워크플로우
37
39
  | `commit` | `bylane-commit-agent` |
38
40
  | `pr` | `bylane-pr-agent` |
39
41
  | `review` | `bylane-review-agent` |
42
+ | `review-loop` | `bylane-review-loop` |
40
43
  | `respond` | `bylane-respond-agent` |
44
+ | `respond-loop` | `bylane-respond-loop` |
41
45
  | `notify` | `bylane-notify-agent` |
42
46
  | `status` | `.bylane/state/` 파일 읽어 한 줄 요약 출력 |
43
47
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elyun/bylane",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Frontend development harness for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,7 +11,8 @@
11
11
  "monitor": "node src/monitor/index.js",
12
12
  "test": "vitest run",
13
13
  "test:watch": "vitest",
14
- "release": "bash scripts/release.sh"
14
+ "release": "bash scripts/release.sh",
15
+ "prepare": "git config core.hooksPath .githooks && chmod +x .githooks/pre-commit"
15
16
  },
16
17
  "dependencies": {
17
18
  "blessed": "^0.1.81",
@@ -1,19 +1,15 @@
1
1
  #!/bin/bash
2
2
  set -e
3
3
 
4
- FORCE_RELEASE=false
5
- for arg in "$@"; do
6
- [ "$arg" = "--force-release" ] && FORCE_RELEASE=true
7
- done
8
- # npm run release --force-release 방식 지원 (npm_config_* 환경변수)
9
- [ -n "$npm_config_force_release" ] && FORCE_RELEASE=true
4
+ VERSION="v$(node -p "require('./package.json').version")"
10
5
 
11
- echo "byLane 릴리즈 시작..."
6
+ echo "byLane npm 배포 시작: $VERSION"
12
7
 
13
- # 작업 디렉토리 클린 확인 (--force-release 건너뜀)
14
- if [ "$FORCE_RELEASE" = false ] && [ -n "$(git status --short)" ]; then
15
- echo "오류: 커밋되지 않은 변경사항이 있습니다. 먼저 커밋하거나 --force-release 를 사용하세요."
16
- git status --short
8
+ # 작업 디렉토리 클린 확인 (untracked 파일 제외, 수정/스테이징된 파일만 체크)
9
+ DIRTY=$(git status --short | grep -v '^?' || true)
10
+ if [ -n "$DIRTY" ]; then
11
+ echo "오류: 커밋되지 않은 변경사항이 있습니다. 먼저 커밋하고 푸시하세요."
12
+ echo "$DIRTY"
17
13
  exit 1
18
14
  fi
19
15
 
@@ -21,42 +17,10 @@ fi
21
17
  echo "테스트 실행 중..."
22
18
  npm test
23
19
 
24
- if [ "$FORCE_RELEASE" = true ]; then
25
- # 현재 package.json 버전 그대로 사용
26
- NEW_VERSION="v$(node -p "require('./package.json').version")"
27
- echo "현재 버전으로 배포: $NEW_VERSION"
28
-
29
- # 태그가 없으면 생성
30
- if ! git tag | grep -q "^$NEW_VERSION$"; then
31
- git tag "$NEW_VERSION"
32
- git push origin "$NEW_VERSION"
33
- echo "태그 생성: $NEW_VERSION"
34
- else
35
- echo "태그 이미 존재: $NEW_VERSION"
36
- fi
37
- else
38
- # 마이너 버전 올리기
39
- echo "버전 올리는 중..."
40
- NEW_VERSION=$(npm version minor --no-git-tag-version)
41
- echo "새 버전: $NEW_VERSION"
42
-
43
- # package-lock.json 업데이트
44
- npm install --package-lock-only 2>/dev/null || true
45
-
46
- # 커밋 + 태그
47
- git add package.json package-lock.json
48
- git commit -m "chore: release $NEW_VERSION"
49
- git tag "$NEW_VERSION"
50
-
51
- # GitHub 푸시
52
- echo "GitHub 푸시 중..."
53
- git push origin main --tags
54
- fi
55
-
56
20
  # npm 배포 (2FA 필요 시 프롬프트)
57
21
  echo "npm 배포 중... (2FA 코드가 필요할 수 있습니다)"
58
22
  npm publish --access public
59
23
 
60
24
  echo ""
61
- echo "릴리즈 완료: $NEW_VERSION"
25
+ echo "배포 완료: $VERSION"
62
26
  echo "https://www.npmjs.com/package/@elyun/bylane"
@@ -0,0 +1,109 @@
1
+ ---
2
+ name: bylane-respond-loop
3
+ description: 5분 주기로 내 PR에 달린 리뷰/코멘트를 감지하여 자동으로 반박하거나 반영한다.
4
+ ---
5
+
6
+ # Respond Loop Agent
7
+
8
+ ## 개요
9
+
10
+ 백그라운드 폴러(`src/respond-loop.js`)가 5분마다 내 PR에 달린 리뷰/코멘트를 감지하여
11
+ `.bylane/state/respond-queue.json`에 기록한다. 이 skill은 해당 큐를 감시하다가
12
+ `status: "pending"` 항목이 생기면 `bylane-respond-agent`를 실행한다.
13
+
14
+ 감지 대상:
15
+ - `CHANGES_REQUESTED` 리뷰
16
+ - 일반 코멘트 (COMMENTED)
17
+ - 대응 후 추가된 새 코멘트 (updatedAt 변경 감지)
18
+
19
+ ## 시작
20
+
21
+ ### 1. 폴러 시작 (백그라운드)
22
+
23
+ ```bash
24
+ node src/respond-loop.js &
25
+ echo "폴러 PID: $!"
26
+ ```
27
+
28
+ ### 2. 큐 감시 루프
29
+
30
+ pending PR이 생길 때마다 respond-agent를 실행한다:
31
+
32
+ ```bash
33
+ node -e "
34
+ import('./src/state.js').then(({readState}) => {
35
+ const q = readState('respond-queue', '.bylane/state')
36
+ const pending = (q?.queue ?? []).filter(p => p.status === 'pending')
37
+ console.log(JSON.stringify(pending))
38
+ })
39
+ "
40
+ ```
41
+
42
+ pending 항목이 있으면 각 PR에 대해:
43
+
44
+ 1. `hasChangesRequested` 여부 확인:
45
+ - `true` → 사용자에게 `accept` / `rebut` 모드 선택 요청 후 `bylane-respond-agent` 실행
46
+ - `false` (코멘트만) → 코멘트 내용 확인 후 `accept` / `rebut` 모드 자동 판단 또는 사용자에게 선택 요청
47
+
48
+ 2. `bylane-respond-agent` skill 실행 (PR 번호 + 모드 전달)
49
+
50
+ 3. 완료 후 큐 항목을 `status: "responded"`로 업데이트:
51
+
52
+ ```bash
53
+ node -e "
54
+ import('./src/state.js').then(({readState, writeState}) => {
55
+ const q = readState('respond-queue', '.bylane/state')
56
+ const queue = (q?.queue ?? []).map(p =>
57
+ p.number === PR_NUMBER
58
+ ? { ...p, status: 'responded', respondedAt: new Date().toISOString() }
59
+ : p
60
+ )
61
+ writeState('respond-queue', { status: 'running', queue }, '.bylane/state')
62
+ })
63
+ "
64
+ ```
65
+
66
+ 4. 다음 pending 항목으로 반복. pending 없으면 30초 대기 후 재확인.
67
+
68
+ ## 큐 항목 스키마
69
+
70
+ `.bylane/state/respond-queue.json`:
71
+
72
+ ```json
73
+ {
74
+ "agent": "respond-queue",
75
+ "status": "running",
76
+ "queue": [
77
+ {
78
+ "number": 45,
79
+ "title": "Add dark mode toggle",
80
+ "url": "https://github.com/owner/repo/pull/45",
81
+ "branch": "feature/45-dark-mode",
82
+ "updatedAt": "2026-04-05T10:00:00Z",
83
+ "hasChangesRequested": true,
84
+ "status": "pending",
85
+ "detectedAt": "2026-04-05T10:01:00Z"
86
+ }
87
+ ]
88
+ }
89
+ ```
90
+
91
+ `status` 값:
92
+ - `pending` — 대응 대기 중
93
+ - `responding` — 현재 respond-agent 실행 중
94
+ - `responded` — 대응 완료 (새 코멘트 오면 pending으로 재전환)
95
+
96
+ ## 재감지
97
+
98
+ 폴러가 이미 `responded` 상태인 PR의 `updatedAt`이 변경된 것을 감지하면
99
+ 자동으로 `status: "pending"`으로 되돌린다.
100
+
101
+ ## 종료
102
+
103
+ ```bash
104
+ kill $(pgrep -f respond-loop.js)
105
+ ```
106
+
107
+ ## 수동 실행
108
+
109
+ `/bylane respond-loop`
@@ -16,6 +16,34 @@ description: PR의 diff를 분석하여 코드 리뷰 코멘트를 작성한다.
16
16
  | `"api"` | REST API + `$GITHUB_TOKEN` |
17
17
  | `"auto"` (기본) | MCP → CLI → API 순서로 시도 |
18
18
 
19
+ ## 리뷰 템플릿 로드
20
+
21
+ 실행 전 `.bylane/bylane.json`의 `review` 설정 읽기:
22
+
23
+ ```bash
24
+ node -e "
25
+ import('./src/config.js').then(({loadConfig}) => {
26
+ const c = loadConfig()
27
+ console.log(JSON.stringify(c.review, null, 2))
28
+ })
29
+ "
30
+ ```
31
+
32
+ 설정 항목:
33
+ - `model` — 리뷰에 사용할 모델 (기본: `claude-sonnet-4-6`)
34
+ - `language` — 리뷰 언어 (기본: `ko`)
35
+ - `includeModel` — 푸터에 모델명 포함 여부
36
+ - `includeCodeExample` — Before/After 코드 예시 포함 여부
37
+ - `templateFile` — 커스텀 템플릿 파일 경로 (비어있으면 `templates/review-template.md` 사용)
38
+ - `severityEmoji` — 심각도 레이블 커스터마이즈
39
+ - `footer` — 푸터 문자열 (`{model}`, `{date}` 치환 가능)
40
+
41
+ 커스텀 템플릿이 있으면 로드:
42
+ ```bash
43
+ # templateFile이 설정된 경우
44
+ cat TEMPLATE_FILE_PATH
45
+ ```
46
+
19
47
  ## 입력
20
48
 
21
49
  PR 번호 (`.bylane/state/pr-agent.json`에서 자동 로드, 또는 수동 전달)
@@ -36,7 +64,6 @@ node -e "import('./src/state.js').then(({writeState})=>writeState('review-agent'
36
64
  **CLI:**
37
65
  ```bash
38
66
  gh pr diff PR_NUMBER
39
- # 파일 목록
40
67
  gh pr view PR_NUMBER --json files
41
68
  ```
42
69
 
@@ -54,25 +81,45 @@ node -e "import('./src/state.js').then(({writeState})=>writeState('review-agent'
54
81
  - 코딩 컨벤션 위반
55
82
  - 테스트 커버리지 누락
56
83
 
57
- 3. 리뷰 코멘트 심각도:
58
- - **CRITICAL**: 즉시 수정 필요 (버그, 보안)
59
- - **HIGH**: 수정 강력 권장
60
- - **MEDIUM**: 개선 권장
61
- - **LOW**: 선택적 개선
84
+ 3. 코멘트 작성 규칙 (템플릿 적용):
85
+
86
+ 코멘트 형식:
87
+ ```
88
+ {severityEmoji.SEVERITY} {title}
89
+
90
+ {description}
91
+
92
+ [includeCodeExample=true인 경우]
93
+ **Before:**
94
+ ```lang
95
+ // 문제 코드
96
+ ```
97
+ **After:**
98
+ ```lang
99
+ // 개선 코드
100
+ ```
101
+
102
+ {suggestion}
103
+ ```
104
+
105
+ 리뷰 언어(`language`)에 맞게 작성. `ko`이면 한국어, `en`이면 영어.
106
+
107
+ 4. 전체 요약 생성:
108
+ - 심각도별 건수 표
109
+ - 주요 발견사항
110
+ - 종합 의견
111
+ - 푸터: `review.footer`의 `{model}`을 실제 모델명으로, `{date}`를 현재 날짜로 치환
62
112
 
63
- 4. 리뷰 제출:
113
+ 5. 리뷰 제출 (CRITICAL/HIGH 없으면 `APPROVE`, 있으면 `REQUEST_CHANGES`):
64
114
 
65
115
  **MCP:**
66
- → GitHub MCP `create_review` 도구 사용 (event: `APPROVE` 또는 `REQUEST_CHANGES`)
116
+ → GitHub MCP `create_review` 도구 사용
67
117
 
68
118
  **CLI:**
69
119
  ```bash
70
- # 승인
71
120
  gh pr review PR_NUMBER --approve --body "REVIEW_BODY"
72
- # 변경 요청
121
+ # 또는
73
122
  gh pr review PR_NUMBER --request-changes --body "REVIEW_BODY"
74
- # 개별 라인 코멘트
75
- gh pr comment PR_NUMBER --body "COMMENT"
76
123
  ```
77
124
 
78
125
  **API:**
@@ -84,7 +131,7 @@ node -e "import('./src/state.js').then(({writeState})=>writeState('review-agent'
84
131
  -d '{"body":"REVIEW_BODY","event":"APPROVE","comments":[]}'
85
132
  ```
86
133
 
87
- 5. 상태 업데이트:
134
+ 6. 상태 업데이트:
88
135
  ```bash
89
136
  node -e "import('./src/state.js').then(({writeState})=>writeState('review-agent',{status:'completed',progress:100,approved:APPROVED_BOOL,commentCount:COMMENT_COUNT}))"
90
137
  ```
@@ -0,0 +1,105 @@
1
+ ---
2
+ name: bylane-review-loop
3
+ description: 5분 주기로 GitHub review 요청된 PR을 감지하여 자동으로 리뷰한다. 재요청 포함.
4
+ ---
5
+
6
+ # Review Loop Agent
7
+
8
+ ## 개요
9
+
10
+ 백그라운드 폴러(`src/review-loop.js`)가 5분마다 GitHub을 체크하여 review 요청된 PR을
11
+ `.bylane/state/review-queue.json`에 기록한다. 이 skill은 해당 큐를 감시하다가
12
+ `status: "pending"` 항목이 생기면 `bylane-review-agent`를 실행한다.
13
+
14
+ ## 시작
15
+
16
+ ### 1. 폴러 시작 (백그라운드)
17
+
18
+ ```bash
19
+ node src/review-loop.js &
20
+ echo "폴러 PID: $!"
21
+ ```
22
+
23
+ 또는 별도 터미널에서:
24
+ ```bash
25
+ node src/review-loop.js
26
+ ```
27
+
28
+ ### 2. 큐 감시 루프
29
+
30
+ 아래 루프를 실행하면서 pending PR이 생길 때마다 review-agent를 실행한다:
31
+
32
+ ```bash
33
+ # 큐 확인
34
+ node -e "
35
+ import('./src/state.js').then(({readState}) => {
36
+ const q = readState('review-queue', '.bylane/state')
37
+ const pending = (q?.queue ?? []).filter(p => p.status === 'pending')
38
+ console.log(JSON.stringify(pending))
39
+ })
40
+ "
41
+ ```
42
+
43
+ pending 항목이 있으면 각 PR에 대해:
44
+ 1. `bylane-review-agent` skill 실행 (PR 번호 전달)
45
+ 2. 리뷰 완료 후 큐 항목을 `status: "reviewed"`로 업데이트:
46
+
47
+ ```bash
48
+ node -e "
49
+ import('./src/state.js').then(({readState, writeState}) => {
50
+ const q = readState('review-queue', '.bylane/state')
51
+ const queue = (q?.queue ?? []).map(p =>
52
+ p.number === PR_NUMBER ? { ...p, status: 'reviewed', reviewedAt: new Date().toISOString() } : p
53
+ )
54
+ writeState('review-queue', { status: 'running', queue }, '.bylane/state')
55
+ })
56
+ "
57
+ ```
58
+
59
+ 3. 다음 pending 항목으로 반복
60
+ 4. pending 없으면 30초 대기 후 재확인
61
+
62
+ ## 큐 항목 스키마
63
+
64
+ `.bylane/state/review-queue.json`:
65
+
66
+ ```json
67
+ {
68
+ "agent": "review-queue",
69
+ "status": "running",
70
+ "queue": [
71
+ {
72
+ "number": 45,
73
+ "title": "Add dark mode toggle",
74
+ "url": "https://github.com/owner/repo/pull/45",
75
+ "branch": "feature/45-dark-mode",
76
+ "updatedAt": "2026-04-05T10:00:00Z",
77
+ "status": "pending",
78
+ "detectedAt": "2026-04-05T10:01:00Z"
79
+ }
80
+ ]
81
+ }
82
+ ```
83
+
84
+ `status` 값:
85
+ - `pending` — 리뷰 대기 중
86
+ - `reviewing` — 현재 review-agent 실행 중
87
+ - `reviewed` — 리뷰 완료 (updatedAt 변경 시 pending으로 재전환됨)
88
+
89
+ ## 재요청 처리
90
+
91
+ 폴러가 이미 `reviewed` 상태인 PR의 `updatedAt`이 변경된 것을 감지하면
92
+ 자동으로 `status: "pending"`으로 되돌린다.
93
+
94
+ ## 종료
95
+
96
+ ```bash
97
+ # 폴러 종료
98
+ kill $(pgrep -f review-loop.js)
99
+
100
+ # 또는 폴러 터미널에서 Ctrl+C
101
+ ```
102
+
103
+ ## 수동 실행
104
+
105
+ `/bylane review-loop`
package/src/config.js CHANGED
@@ -39,6 +39,20 @@ export const DEFAULT_CONFIG = {
39
39
  method: 'auto',
40
40
  owner: '',
41
41
  repo: ''
42
+ },
43
+ review: {
44
+ model: 'claude-sonnet-4-6',
45
+ language: 'ko',
46
+ includeModel: true,
47
+ includeCodeExample: true,
48
+ templateFile: '',
49
+ severityEmoji: {
50
+ CRITICAL: '[CRITICAL]',
51
+ HIGH: '[HIGH]',
52
+ MEDIUM: '[MEDIUM]',
53
+ LOW: '[LOW]'
54
+ },
55
+ footer: 'Reviewed by byLane · model: {model}'
42
56
  }
43
57
  }
44
58
 
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * respond-loop.js
4
+ * 5분 주기로 내 PR에 달린 리뷰/코멘트를 감지해 .bylane/state/respond-queue.json에 기록한다.
5
+ * REQUEST_CHANGES 및 새 코멘트 포함.
6
+ */
7
+ import { execSync } from 'child_process'
8
+ import { mkdirSync } from 'fs'
9
+ import { writeState, readState, appendLog } from './state.js'
10
+ import { loadConfig } from './config.js'
11
+
12
+ const INTERVAL_MS = 5 * 60 * 1000
13
+ const STATE_DIR = '.bylane/state'
14
+
15
+ mkdirSync(STATE_DIR, { recursive: true })
16
+
17
+ function fetchMyPRsWithReviews(method, owner, repo) {
18
+ // CLI
19
+ if (method === 'cli' || method === 'auto') {
20
+ try {
21
+ const out = execSync(
22
+ 'gh pr list --author @me --json number,title,url,headRefName,updatedAt,reviewDecision,reviews --limit 50',
23
+ { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
24
+ )
25
+ const prs = JSON.parse(out)
26
+ return prs.filter(pr =>
27
+ pr.reviewDecision === 'CHANGES_REQUESTED' ||
28
+ (pr.reviews ?? []).some(r => r.state === 'CHANGES_REQUESTED' || r.state === 'COMMENTED')
29
+ )
30
+ } catch {
31
+ if (method === 'cli') throw new Error('gh CLI 실패')
32
+ }
33
+ }
34
+
35
+ // API
36
+ if (method === 'api' || method === 'auto') {
37
+ const token = process.env.GITHUB_TOKEN
38
+ if (!token) throw new Error('GITHUB_TOKEN 환경변수가 설정되지 않았습니다.')
39
+ if (!owner || !repo) throw new Error('github.owner, github.repo 설정이 필요합니다.')
40
+
41
+ const out = execSync(
42
+ `curl -s -H "Authorization: Bearer ${token}" ` +
43
+ `"https://api.github.com/repos/${owner}/${repo}/pulls?state=open&per_page=50"`,
44
+ { encoding: 'utf8' }
45
+ )
46
+ const prs = JSON.parse(out)
47
+
48
+ return prs
49
+ .filter(pr => pr.requested_reviewers?.length > 0 || pr.review_comments > 0)
50
+ .map(pr => ({
51
+ number: pr.number,
52
+ title: pr.title,
53
+ url: pr.html_url,
54
+ headRefName: pr.head.ref,
55
+ updatedAt: pr.updated_at,
56
+ reviewDecision: null
57
+ }))
58
+ }
59
+
60
+ return []
61
+ }
62
+
63
+ function fetchReviewComments(method, owner, repo, prNumber) {
64
+ if (method === 'cli' || method === 'auto') {
65
+ try {
66
+ const out = execSync(
67
+ `gh pr view ${prNumber} --json reviews,comments`,
68
+ { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
69
+ )
70
+ return JSON.parse(out)
71
+ } catch {
72
+ if (method === 'cli') throw new Error('gh CLI 실패')
73
+ }
74
+ }
75
+
76
+ if (method === 'api' || method === 'auto') {
77
+ const token = process.env.GITHUB_TOKEN
78
+ const reviewsOut = execSync(
79
+ `curl -s -H "Authorization: Bearer ${token}" ` +
80
+ `"https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/reviews"`,
81
+ { encoding: 'utf8' }
82
+ )
83
+ return { reviews: JSON.parse(reviewsOut), comments: [] }
84
+ }
85
+
86
+ return { reviews: [], comments: [] }
87
+ }
88
+
89
+ function loadQueue() {
90
+ try {
91
+ const s = readState('respond-queue', STATE_DIR)
92
+ return s?.queue ?? []
93
+ } catch {
94
+ return []
95
+ }
96
+ }
97
+
98
+ function saveQueue(queue) {
99
+ writeState('respond-queue', { status: 'running', queue }, STATE_DIR)
100
+ }
101
+
102
+ async function poll() {
103
+ const config = loadConfig()
104
+ const method = config.github?.method ?? 'auto'
105
+ const owner = config.github?.owner ?? ''
106
+ const repo = config.github?.repo ?? ''
107
+
108
+ appendLog('respond-loop', `폴링 시작 (method: ${method})`, STATE_DIR)
109
+
110
+ let prs
111
+ try {
112
+ prs = fetchMyPRsWithReviews(method, owner, repo)
113
+ } catch (err) {
114
+ appendLog('respond-loop', `오류: ${err.message}`, STATE_DIR)
115
+ return
116
+ }
117
+
118
+ const queue = loadQueue()
119
+ const queueMap = Object.fromEntries(queue.map(q => [q.number, q]))
120
+
121
+ let newCount = 0
122
+ for (const pr of prs) {
123
+ let detail
124
+ try {
125
+ detail = fetchReviewComments(method, owner, repo, pr.number)
126
+ } catch {
127
+ detail = { reviews: [], comments: [] }
128
+ }
129
+
130
+ const hasChangesRequested = (detail.reviews ?? []).some(r => r.state === 'CHANGES_REQUESTED')
131
+ const hasComments = (detail.reviews ?? []).some(r => r.state === 'COMMENTED') ||
132
+ (detail.comments ?? []).length > 0
133
+ const needsResponse = hasChangesRequested || hasComments
134
+
135
+ if (!needsResponse) continue
136
+
137
+ const existing = queueMap[pr.number]
138
+ const isNew = !existing
139
+ const isUpdated = existing &&
140
+ existing.updatedAt !== pr.updatedAt &&
141
+ existing.status === 'responded'
142
+
143
+ if (isNew || isUpdated) {
144
+ queueMap[pr.number] = {
145
+ number: pr.number,
146
+ title: pr.title,
147
+ url: pr.url,
148
+ branch: pr.headRefName,
149
+ updatedAt: pr.updatedAt,
150
+ hasChangesRequested,
151
+ status: 'pending',
152
+ detectedAt: new Date().toISOString()
153
+ }
154
+ newCount++
155
+ appendLog('respond-loop',
156
+ `${isNew ? '새' : '재요청'} PR #${pr.number}: ${pr.title} ${hasChangesRequested ? '[CHANGES_REQUESTED]' : '[COMMENTED]'}`,
157
+ STATE_DIR
158
+ )
159
+ }
160
+ }
161
+
162
+ saveQueue(Object.values(queueMap))
163
+
164
+ if (newCount > 0) {
165
+ appendLog('respond-loop', `${newCount}개 PR이 큐에 추가됨`, STATE_DIR)
166
+ } else {
167
+ appendLog('respond-loop', '새 리뷰 코멘트 없음', STATE_DIR)
168
+ }
169
+ }
170
+
171
+ // 초기 실행
172
+ poll()
173
+
174
+ // 5분 주기 폴링
175
+ const timer = setInterval(poll, INTERVAL_MS)
176
+
177
+ // 종료 처리
178
+ process.on('SIGINT', () => {
179
+ clearInterval(timer)
180
+ writeState('respond-loop', { status: 'stopped' }, STATE_DIR)
181
+ process.exit(0)
182
+ })
183
+ process.on('SIGTERM', () => {
184
+ clearInterval(timer)
185
+ writeState('respond-loop', { status: 'stopped' }, STATE_DIR)
186
+ process.exit(0)
187
+ })
188
+
189
+ writeState('respond-loop', { status: 'running', startedAt: new Date().toISOString() }, STATE_DIR)
190
+ console.log('respond-loop 시작. Ctrl+C로 종료.')
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * review-loop.js
4
+ * 5분 주기로 GitHub review 요청된 PR을 감지해 .bylane/state/review-queue.json에 기록한다.
5
+ */
6
+ import { execSync } from 'child_process'
7
+ import { mkdirSync } from 'fs'
8
+ import { writeState, readState, appendLog } from './state.js'
9
+ import { loadConfig } from './config.js'
10
+
11
+ const INTERVAL_MS = 5 * 60 * 1000
12
+ const STATE_DIR = '.bylane/state'
13
+
14
+ mkdirSync(STATE_DIR, { recursive: true })
15
+
16
+ function fetchPendingReviews(method, owner, repo) {
17
+ // CLI
18
+ if (method === 'cli' || method === 'auto') {
19
+ try {
20
+ const out = execSync(
21
+ 'gh pr list --review-requested @me --json number,title,url,headRefName,updatedAt --limit 50',
22
+ { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
23
+ )
24
+ return JSON.parse(out)
25
+ } catch {
26
+ if (method === 'cli') throw new Error('gh CLI 실패')
27
+ }
28
+ }
29
+
30
+ // API
31
+ if (method === 'api' || method === 'auto') {
32
+ const token = process.env.GITHUB_TOKEN
33
+ if (!token) throw new Error('GITHUB_TOKEN 환경변수가 설정되지 않았습니다.')
34
+ if (!owner || !repo) throw new Error('github.owner, github.repo 설정이 필요합니다.')
35
+
36
+ const out = execSync(
37
+ `curl -s -H "Authorization: Bearer ${token}" ` +
38
+ `"https://api.github.com/repos/${owner}/${repo}/pulls?state=open&per_page=50"`,
39
+ { encoding: 'utf8' }
40
+ )
41
+ const prs = JSON.parse(out)
42
+ return prs
43
+ .filter(pr => pr.requested_reviewers?.length > 0)
44
+ .map(pr => ({
45
+ number: pr.number,
46
+ title: pr.title,
47
+ url: pr.html_url,
48
+ headRefName: pr.head.ref,
49
+ updatedAt: pr.updated_at
50
+ }))
51
+ }
52
+
53
+ return []
54
+ }
55
+
56
+ function loadQueue() {
57
+ try {
58
+ const s = readState('review-queue', STATE_DIR)
59
+ return s?.queue ?? []
60
+ } catch {
61
+ return []
62
+ }
63
+ }
64
+
65
+ function saveQueue(queue) {
66
+ writeState('review-queue', { status: 'running', queue }, STATE_DIR)
67
+ }
68
+
69
+ async function poll() {
70
+ const config = loadConfig()
71
+ const method = config.github?.method ?? 'auto'
72
+ const owner = config.github?.owner ?? ''
73
+ const repo = config.github?.repo ?? ''
74
+
75
+ appendLog('review-loop', `폴링 시작 (method: ${method})`, STATE_DIR)
76
+
77
+ let prs
78
+ try {
79
+ prs = fetchPendingReviews(method, owner, repo)
80
+ } catch (err) {
81
+ appendLog('review-loop', `오류: ${err.message}`, STATE_DIR)
82
+ return
83
+ }
84
+
85
+ const queue = loadQueue()
86
+ const queueMap = Object.fromEntries(queue.map(q => [q.number, q]))
87
+
88
+ let newCount = 0
89
+ for (const pr of prs) {
90
+ const existing = queueMap[pr.number]
91
+ const isNew = !existing
92
+ const isUpdated = existing && existing.updatedAt !== pr.updatedAt && existing.status === 'reviewed'
93
+
94
+ if (isNew || isUpdated) {
95
+ queueMap[pr.number] = {
96
+ number: pr.number,
97
+ title: pr.title,
98
+ url: pr.url,
99
+ branch: pr.headRefName,
100
+ updatedAt: pr.updatedAt,
101
+ status: 'pending',
102
+ detectedAt: new Date().toISOString()
103
+ }
104
+ newCount++
105
+ appendLog('review-loop', `${isNew ? '새' : '재요청'} PR #${pr.number}: ${pr.title}`, STATE_DIR)
106
+ }
107
+ }
108
+
109
+ saveQueue(Object.values(queueMap))
110
+
111
+ if (newCount > 0) {
112
+ appendLog('review-loop', `${newCount}개 PR이 큐에 추가됨`, STATE_DIR)
113
+ } else {
114
+ appendLog('review-loop', '새 review 요청 없음', STATE_DIR)
115
+ }
116
+ }
117
+
118
+ // 초기 실행
119
+ poll()
120
+
121
+ // 5분 주기 폴링
122
+ const timer = setInterval(poll, INTERVAL_MS)
123
+
124
+ // 종료 처리
125
+ process.on('SIGINT', () => {
126
+ clearInterval(timer)
127
+ writeState('review-loop', { status: 'stopped' }, STATE_DIR)
128
+ process.exit(0)
129
+ })
130
+ process.on('SIGTERM', () => {
131
+ clearInterval(timer)
132
+ writeState('review-loop', { status: 'stopped' }, STATE_DIR)
133
+ process.exit(0)
134
+ })
135
+
136
+ writeState('review-loop', { status: 'running', startedAt: new Date().toISOString() }, STATE_DIR)
137
+ console.log('review-loop 시작. Ctrl+C로 종료.')
@@ -0,0 +1,86 @@
1
+ # byLane 리뷰 템플릿
2
+
3
+ 이 파일을 복사해 프로젝트별로 커스터마이즈하세요.
4
+ `.bylane/bylane.json`의 `review.templateFile`에 경로를 지정하면 적용됩니다.
5
+
6
+ ---
7
+
8
+ ## 코멘트 형식
9
+
10
+ 각 리뷰 코멘트는 아래 형식을 따릅니다:
11
+
12
+ ```
13
+ {severity} {title}
14
+
15
+ {description}
16
+
17
+ {code_example}
18
+
19
+ {suggestion}
20
+ ```
21
+
22
+ ### 필드 설명
23
+
24
+ - `{severity}` — 심각도 레이블 (`[CRITICAL]` / `[HIGH]` / `[MEDIUM]` / `[LOW]`)
25
+ - `{title}` — 문제 제목 (한 줄)
26
+ - `{description}` — 문제 설명 및 이유
27
+ - `{code_example}` — 문제가 있는 코드 예시 (선택, `review.includeCodeExample: false`로 비활성)
28
+ - `{suggestion}` — 수정 방법 또는 개선 예시 코드
29
+
30
+ ---
31
+
32
+ ## 심각도 기준
33
+
34
+ | 심각도 | 기준 | 처리 |
35
+ |--------|------|------|
36
+ | CRITICAL | 버그, 보안 취약점, 데이터 손실 가능성 | 즉시 수정 필요, PR 차단 |
37
+ | HIGH | 잘못된 로직, 성능 심각 저하 | 수정 강력 권장 |
38
+ | MEDIUM | 코드 품질, 가독성, 테스트 누락 | 개선 권장 |
39
+ | LOW | 네이밍, 스타일, 선택적 개선 | 참고용 |
40
+
41
+ ---
42
+
43
+ ## 코드 예시 형식
44
+
45
+ **Before (문제 코드):**
46
+ ```language
47
+ // 문제가 있는 코드
48
+ ```
49
+
50
+ **After (수정 제안):**
51
+ ```language
52
+ // 개선된 코드
53
+ ```
54
+
55
+ ---
56
+
57
+ ## 전체 리뷰 요약 형식
58
+
59
+ ```
60
+ ## 코드 리뷰 요약
61
+
62
+ | 심각도 | 건수 |
63
+ |--------|------|
64
+ | CRITICAL | N |
65
+ | HIGH | N |
66
+ | MEDIUM | N |
67
+ | LOW | N |
68
+
69
+ ### 주요 발견사항
70
+ - ...
71
+
72
+ ### 종합 의견
73
+ ...
74
+
75
+ ---
76
+ {footer}
77
+ ```
78
+
79
+ ---
80
+
81
+ ## 푸터
82
+
83
+ 기본값: `Reviewed by byLane · model: {model}`
84
+
85
+ `{model}` — 실제 사용된 모델명으로 자동 치환됩니다.
86
+ 커스터마이즈 예시: `AI 리뷰 by byLane (claude-sonnet-4-6) · {date}`