@elyun/bylane 1.3.0 → 1.5.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.
- package/.githooks/pre-commit +88 -0
- package/commands/bylane.md +4 -0
- package/package.json +3 -2
- package/scripts/release.sh +8 -44
- package/skills/orchestrator.md +20 -0
- package/skills/respond-loop.md +109 -0
- package/skills/review-agent.md +60 -13
- package/skills/review-loop.md +105 -0
- package/skills/setup.md +26 -0
- package/src/config.js +30 -0
- package/src/respond-loop.js +190 -0
- package/src/review-loop.js +137 -0
- package/templates/review-template.md +86 -0
|
@@ -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
|
package/commands/bylane.md
CHANGED
|
@@ -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
|
+
"version": "1.5.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",
|
package/scripts/release.sh
CHANGED
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
set -e
|
|
3
3
|
|
|
4
|
-
|
|
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
|
-
# 작업 디렉토리 클린 확인 (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 "
|
|
25
|
+
echo "배포 완료: $VERSION"
|
|
62
26
|
echo "https://www.npmjs.com/package/@elyun/bylane"
|
package/skills/orchestrator.md
CHANGED
|
@@ -14,6 +14,25 @@ description: byLane 메인 오케스트레이터. 자연어 의도를 파싱해
|
|
|
14
14
|
1. `.bylane/bylane.json` 로드. 없으면 즉시 `bylane-setup` 스킬 실행.
|
|
15
15
|
2. `.bylane/state/` 디렉토리 확인. 없으면 생성.
|
|
16
16
|
|
|
17
|
+
## 에이전트별 모델 결정
|
|
18
|
+
|
|
19
|
+
각 에이전트 실행 전 사용할 모델을 config에서 읽는다:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
node -e "
|
|
23
|
+
import('./src/config.js').then(({loadConfig, getAgentModel}) => {
|
|
24
|
+
const config = loadConfig()
|
|
25
|
+
const agents = [
|
|
26
|
+
'orchestrator','issue-agent','code-agent','test-agent',
|
|
27
|
+
'commit-agent','pr-agent','review-agent','respond-agent','notify-agent'
|
|
28
|
+
]
|
|
29
|
+
agents.forEach(a => console.log(a + ': ' + getAgentModel(config, a)))
|
|
30
|
+
})
|
|
31
|
+
"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
에이전트 호출 시 해당 모델을 `model` 파라미터로 전달한다.
|
|
35
|
+
|
|
17
36
|
## 의도 파싱 규칙
|
|
18
37
|
|
|
19
38
|
입력을 분석하여 아래 중 하나로 분류:
|
|
@@ -32,6 +51,7 @@ description: byLane 메인 오케스트레이터. 자연어 의도를 파싱해
|
|
|
32
51
|
## 에이전트 실행 방법
|
|
33
52
|
|
|
34
53
|
각 에이전트를 순서대로 Agent 도구로 호출한다. 이전 에이전트의 출력을 다음 에이전트의 입력으로 전달한다.
|
|
54
|
+
**config에서 읽은 모델을 `model` 파라미터로 반드시 전달한다.**
|
|
35
55
|
|
|
36
56
|
상태 기록 (각 에이전트 시작 전):
|
|
37
57
|
```bash
|
|
@@ -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`
|
package/skills/review-agent.md
CHANGED
|
@@ -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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
113
|
+
5. 리뷰 제출 (CRITICAL/HIGH 없으면 `APPROVE`, 있으면 `REQUEST_CHANGES`):
|
|
64
114
|
|
|
65
115
|
**MCP:**
|
|
66
|
-
→ GitHub MCP `create_review` 도구 사용
|
|
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
|
-
|
|
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/skills/setup.md
CHANGED
|
@@ -107,6 +107,32 @@ Linear API Key는 환경변수명(`LINEAR_API_KEY`)으로 저장. 실제 키값
|
|
|
107
107
|
직접 입력 시 사용 가능한 토큰 목록 안내:
|
|
108
108
|
`{tracker}`, `{type}`, `{issue-number}`, `{custom-id}`, `{title-slug}`, `{date}`, `{username}`
|
|
109
109
|
|
|
110
|
+
## Step 7/7 — 에이전트 모델 설정
|
|
111
|
+
|
|
112
|
+
> 각 에이전트에 사용할 AI 모델을 설정하시겠습니까? (Enter = 기본값 사용)
|
|
113
|
+
|
|
114
|
+
기본값을 보여주고 변경할 항목만 입력받는다:
|
|
115
|
+
|
|
116
|
+
| 에이전트 | 기본 모델 | 권장 용도 |
|
|
117
|
+
|----------|-----------|-----------|
|
|
118
|
+
| default | claude-sonnet-4-6 | 미지정 에이전트 fallback |
|
|
119
|
+
| orchestrator | claude-opus-4-6 | 의도 파싱, 파이프라인 조율 |
|
|
120
|
+
| issue-agent | claude-opus-4-6 | 이슈 생성/분석 |
|
|
121
|
+
| code-agent | claude-sonnet-4-6 | 코드 구현 (핵심) |
|
|
122
|
+
| test-agent | claude-haiku-4-5-20251001 | 테스트 실행 판단 (경량) |
|
|
123
|
+
| commit-agent | claude-haiku-4-5-20251001 | 커밋 메시지 생성 (경량) |
|
|
124
|
+
| pr-agent | claude-haiku-4-5-20251001 | PR 본문 생성 (경량) |
|
|
125
|
+
| review-agent | claude-sonnet-4-6 | 코드 리뷰 (정밀도 중요) |
|
|
126
|
+
| respond-agent | claude-opus-4-6 | 리뷰 대응 (판단력 중요) |
|
|
127
|
+
| notify-agent | claude-haiku-4-5-20251001 | 알림 발송 (경량) |
|
|
128
|
+
|
|
129
|
+
사용 가능한 모델:
|
|
130
|
+
- `claude-opus-4-6` — 최고 성능, 높은 비용
|
|
131
|
+
- `claude-sonnet-4-6` — 균형 (권장)
|
|
132
|
+
- `claude-haiku-4-5-20251001` — 빠르고 저렴
|
|
133
|
+
|
|
134
|
+
`models.default`만 바꾸면 미지정 에이전트 전체에 적용됨을 안내한다.
|
|
135
|
+
|
|
110
136
|
## 저장
|
|
111
137
|
|
|
112
138
|
모든 설정 수집 후:
|
package/src/config.js
CHANGED
|
@@ -39,6 +39,32 @@ export const DEFAULT_CONFIG = {
|
|
|
39
39
|
method: 'auto',
|
|
40
40
|
owner: '',
|
|
41
41
|
repo: ''
|
|
42
|
+
},
|
|
43
|
+
models: {
|
|
44
|
+
default: 'claude-sonnet-4-6',
|
|
45
|
+
orchestrator: 'claude-opus-4-6',
|
|
46
|
+
'issue-agent': 'claude-opus-4-6',
|
|
47
|
+
'code-agent': 'claude-sonnet-4-6',
|
|
48
|
+
'test-agent': 'claude-haiku-4-5-20251001',
|
|
49
|
+
'commit-agent': 'claude-haiku-4-5-20251001',
|
|
50
|
+
'pr-agent': 'claude-haiku-4-5-20251001',
|
|
51
|
+
'review-agent': 'claude-sonnet-4-6',
|
|
52
|
+
'respond-agent': 'claude-opus-4-6',
|
|
53
|
+
'notify-agent': 'claude-haiku-4-5-20251001'
|
|
54
|
+
},
|
|
55
|
+
review: {
|
|
56
|
+
model: 'claude-sonnet-4-6',
|
|
57
|
+
language: 'ko',
|
|
58
|
+
includeModel: true,
|
|
59
|
+
includeCodeExample: true,
|
|
60
|
+
templateFile: '',
|
|
61
|
+
severityEmoji: {
|
|
62
|
+
CRITICAL: '[CRITICAL]',
|
|
63
|
+
HIGH: '[HIGH]',
|
|
64
|
+
MEDIUM: '[MEDIUM]',
|
|
65
|
+
LOW: '[LOW]'
|
|
66
|
+
},
|
|
67
|
+
footer: 'Reviewed by byLane · model: {model}'
|
|
42
68
|
}
|
|
43
69
|
}
|
|
44
70
|
|
|
@@ -71,6 +97,10 @@ function deepMerge(target, source) {
|
|
|
71
97
|
return result
|
|
72
98
|
}
|
|
73
99
|
|
|
100
|
+
export function getAgentModel(config, agentName) {
|
|
101
|
+
return config.models?.[agentName] ?? config.models?.default ?? DEFAULT_CONFIG.models.default
|
|
102
|
+
}
|
|
103
|
+
|
|
74
104
|
export function saveConfig(config, dir = '.bylane') {
|
|
75
105
|
const path = join(dir, 'bylane.json')
|
|
76
106
|
writeFileSync(path, JSON.stringify(config, null, 2))
|
|
@@ -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}`
|