@elyun/bylane 1.15.0 → 1.17.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/commands/bylane-monitor.md +3 -6
- package/commands/bylane-respond-agent.md +21 -30
- package/commands/bylane-respond-loop.md +1 -1
- package/commands/bylane-review-agent.md +112 -120
- package/commands/bylane-review-loop.md +1 -1
- package/package.json +1 -1
- package/src/cli.js +53 -32
- package/src/monitor/index.js +67 -1
- package/src/monitor/layout.js +40 -1
- package/src/respond-loop.js +1 -1
- package/src/review-loop.js +1 -1
- package/templates/review-template.md +17 -47
|
@@ -19,14 +19,11 @@ Claude가 직접 실행하지 않는다. 사용자에게 아래 명령을 안내
|
|
|
19
19
|
모니터 대시보드는 터미널에서 직접 실행하세요:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
|
|
22
|
+
npx @elyun/bylane monitor
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
npm run monitor --prefix ~/.claude/bylane
|
|
29
|
-
```
|
|
25
|
+
> `npx`를 사용하면 항상 최신 버전 모니터가 실행됩니다.
|
|
26
|
+
> 로컬 node_modules의 구버전이 실행되지 않습니다.
|
|
30
27
|
|
|
31
28
|
**종료**: `q` 또는 `Ctrl+C`
|
|
32
29
|
|
|
@@ -16,9 +16,9 @@ description: PR 리뷰 코멘트에 반박하거나 코드를 수정하여 반
|
|
|
16
16
|
| `"api"` | REST API + `$GITHUB_TOKEN` |
|
|
17
17
|
| `"auto"` (기본) | MCP → CLI → API 순서로 시도 |
|
|
18
18
|
|
|
19
|
-
##
|
|
19
|
+
## 대응 템플릿 탐지
|
|
20
20
|
|
|
21
|
-
답글 작성 전 프로젝트 내
|
|
21
|
+
답글 작성 전 프로젝트 내 템플릿을 탐색한다:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
24
|
ls .github/REVIEW_RESPONSE_TEMPLATE.md \
|
|
@@ -26,9 +26,7 @@ ls .github/REVIEW_RESPONSE_TEMPLATE.md \
|
|
|
26
26
|
.github/CONTRIBUTING.md 2>/dev/null | head -1
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
- `CONTRIBUTING.md`가 있으면 코드 기여 가이드라인을 참고하여 대응 톤/형식을 맞춘다.
|
|
31
|
-
- 없으면 기본 형식으로 작성한다.
|
|
29
|
+
템플릿이 있으면 답글 형식을 따른다. 없으면 간결하게 작성한다.
|
|
32
30
|
|
|
33
31
|
## 입력
|
|
34
32
|
|
|
@@ -39,28 +37,25 @@ ls .github/REVIEW_RESPONSE_TEMPLATE.md \
|
|
|
39
37
|
|
|
40
38
|
| 모드 | 동작 |
|
|
41
39
|
|---|---|
|
|
42
|
-
| `auto` (
|
|
40
|
+
| `auto` (기본, 미지정 시) | 각 코멘트를 분석하여 수정 반영 또는 반박을 자동 판단 후 진행 |
|
|
43
41
|
| `accept` | 모든 코멘트를 수정 반영 |
|
|
44
42
|
| `rebut` | 모든 코멘트에 반박 |
|
|
45
|
-
| `manual` | 코멘트별로 사용자에게
|
|
43
|
+
| `manual` | 코멘트별로 사용자에게 선택 요청 |
|
|
46
44
|
|
|
47
45
|
### auto 모드 판단 기준
|
|
48
46
|
|
|
49
|
-
각 코멘트를 분석하여:
|
|
50
47
|
- **반영**: 버그 지적, 명확한 컨벤션 위반, 테스트 누락, 성능 문제
|
|
51
|
-
- **반박**: 의견 차이,
|
|
48
|
+
- **반박**: 의견 차이, 의도된 설계, 스펙 요구사항과 일치하는 경우
|
|
52
49
|
|
|
53
|
-
|
|
50
|
+
실행 전 요약을 먼저 보여주고 확인 후 진행:
|
|
54
51
|
```
|
|
55
52
|
코멘트 #1: [반영] null 체크 누락 → 코드 수정
|
|
56
53
|
코멘트 #2: [반박] 의도된 설계 (이슈 #12 참조)
|
|
57
|
-
코멘트 #3: [반영] 타입 오류
|
|
58
54
|
진행할까요? (y/n)
|
|
59
55
|
```
|
|
60
56
|
|
|
61
57
|
### manual 모드
|
|
62
58
|
|
|
63
|
-
각 코멘트를 보여주고 사용자에게 선택을 요청:
|
|
64
59
|
```
|
|
65
60
|
[코멘트 #1] null 체크가 없습니다.
|
|
66
61
|
→ (1) 수정 반영 (2) 반박 (3) 건너뜀
|
|
@@ -76,8 +71,7 @@ node -e "import('./src/state.js').then(({writeState})=>writeState('respond-agent
|
|
|
76
71
|
|
|
77
72
|
### 리뷰 코멘트 로드
|
|
78
73
|
|
|
79
|
-
**MCP:**
|
|
80
|
-
→ GitHub MCP `list_review_comments` 도구 사용
|
|
74
|
+
**MCP:** → `list_review_comments` 도구 사용
|
|
81
75
|
|
|
82
76
|
**CLI:**
|
|
83
77
|
```bash
|
|
@@ -91,16 +85,12 @@ curl -s \
|
|
|
91
85
|
https://api.github.com/repos/OWNER/REPO/pulls/PR_NUMBER/comments
|
|
92
86
|
```
|
|
93
87
|
|
|
94
|
-
### accept
|
|
88
|
+
### 수정 반영 (accept)
|
|
95
89
|
|
|
96
|
-
1.
|
|
97
|
-
2.
|
|
98
|
-
3. test-agent로 검증
|
|
99
|
-
4. commit-agent로 수정 커밋 (`fix: address review comments`)
|
|
100
|
-
5. 코멘트에 "반영 완료" 답글 게시:
|
|
90
|
+
1. 코드 수정 후 커밋 (`fix: address review comments`)
|
|
91
|
+
2. 해당 코멘트에 짧은 답글 게시:
|
|
101
92
|
|
|
102
|
-
**MCP:**
|
|
103
|
-
→ GitHub MCP `create_review_comment_reply` 도구 사용
|
|
93
|
+
**MCP:** → `create_review_comment_reply` 사용
|
|
104
94
|
|
|
105
95
|
**CLI:**
|
|
106
96
|
```bash
|
|
@@ -116,13 +106,14 @@ curl -s \
|
|
|
116
106
|
-d '{"body":"REPLY_BODY"}'
|
|
117
107
|
```
|
|
118
108
|
|
|
119
|
-
### rebut
|
|
109
|
+
### 반박 (rebut)
|
|
120
110
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
111
|
+
근거를 간결하게 기술한 답글 작성:
|
|
112
|
+
- 의도적 설계 결정: 배경 한 줄 설명
|
|
113
|
+
- 스펙 요구사항과 일치: 이슈 링크 첨부
|
|
114
|
+
- 성능 트레이드오프: 수치 근거
|
|
115
|
+
|
|
116
|
+
답글은 짧고 명확하게. 불필요한 서두 없이 핵심만.
|
|
126
117
|
|
|
127
118
|
## 출력
|
|
128
119
|
|
|
@@ -132,7 +123,7 @@ curl -s \
|
|
|
132
123
|
"agent": "respond-agent",
|
|
133
124
|
"status": "completed",
|
|
134
125
|
"progress": 100,
|
|
135
|
-
"mode": "
|
|
126
|
+
"mode": "auto",
|
|
136
127
|
"resolvedComments": 3,
|
|
137
128
|
"needsMoreWork": false
|
|
138
129
|
}
|
|
@@ -140,4 +131,4 @@ curl -s \
|
|
|
140
131
|
|
|
141
132
|
## 수동 실행
|
|
142
133
|
|
|
143
|
-
`/bylane respond #45`
|
|
134
|
+
`/bylane respond #45`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bylane-review-agent
|
|
3
|
-
description: PR의 diff를 분석하여 코드 리뷰 코멘트를 작성한다.
|
|
3
|
+
description: PR의 diff를 분석하여 코드 라인별 인라인 리뷰 코멘트를 작성한다.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Review Agent
|
|
@@ -28,35 +28,7 @@ ls .github/REVIEW_TEMPLATE.md \
|
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
GitHub 템플릿이 있으면 해당 형식을 **최우선**으로 따른다.
|
|
31
|
-
없으면
|
|
32
|
-
|
|
33
|
-
## 리뷰 템플릿 로드
|
|
34
|
-
|
|
35
|
-
실행 전 `.bylane/bylane.json`의 `review` 설정 읽기:
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
node -e "
|
|
39
|
-
import('./src/config.js').then(({loadConfig}) => {
|
|
40
|
-
const c = loadConfig()
|
|
41
|
-
console.log(JSON.stringify(c.review, null, 2))
|
|
42
|
-
})
|
|
43
|
-
"
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
설정 항목:
|
|
47
|
-
- `model` — 리뷰에 사용할 모델 (기본: `claude-sonnet-4-6`)
|
|
48
|
-
- `language` — 리뷰 언어 (기본: `ko`)
|
|
49
|
-
- `includeModel` — 푸터에 모델명 포함 여부
|
|
50
|
-
- `includeCodeExample` — Before/After 코드 예시 포함 여부
|
|
51
|
-
- `templateFile` — 커스텀 템플릿 파일 경로 (비어있으면 `templates/review-template.md` 사용)
|
|
52
|
-
- `severityEmoji` — 심각도 레이블 커스터마이즈
|
|
53
|
-
- `footer` — 푸터 문자열 (`{model}`, `{date}` 치환 가능)
|
|
54
|
-
|
|
55
|
-
커스텀 템플릿이 있으면 로드:
|
|
56
|
-
```bash
|
|
57
|
-
# templateFile이 설정된 경우
|
|
58
|
-
cat TEMPLATE_FILE_PATH
|
|
59
|
-
```
|
|
31
|
+
없으면 `templates/review-template.md`를 사용한다.
|
|
60
32
|
|
|
61
33
|
## 검사 항목 선택
|
|
62
34
|
|
|
@@ -88,108 +60,128 @@ Enter 또는 아무것도 선택하지 않으면 → `all` (전체 검사)
|
|
|
88
60
|
|
|
89
61
|
| 항목 | 중점 확인 사항 |
|
|
90
62
|
|---|---|
|
|
91
|
-
| `grammar` | 오탈자, 주석 언어 일관성, 변수/함수명
|
|
63
|
+
| `grammar` | 오탈자, 주석 언어 일관성, 변수/함수명 문법 |
|
|
92
64
|
| `domain` | 비즈니스 규칙 위반, 도메인 용어 오용, 로직 정합성 |
|
|
93
65
|
| `code` | 컨벤션 위반, 중복 코드, 복잡도, 불변성, 테스트 커버리지 |
|
|
94
|
-
| `security` | 시크릿 노출, SQL/XSS 인젝션, 인증·인가
|
|
66
|
+
| `security` | 시크릿 노출, SQL/XSS 인젝션, 인증·인가 누락 |
|
|
95
67
|
| `all` | 위 4가지 전체 |
|
|
96
68
|
|
|
97
|
-
|
|
69
|
+
## 실행 전 상태 기록
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
node -e "import('./src/state.js').then(({writeState})=>writeState('review-agent',{status:'in_progress',startedAt:new Date().toISOString(),progress:0,retries:0,log:[]}))"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 실행 흐름
|
|
76
|
+
|
|
77
|
+
### 1. PR diff 로드
|
|
78
|
+
|
|
79
|
+
**MCP:**
|
|
80
|
+
→ `get_pull_request_files` + `get_pull_request` 도구 사용
|
|
81
|
+
|
|
82
|
+
**CLI:**
|
|
83
|
+
```bash
|
|
84
|
+
gh pr diff PR_NUMBER
|
|
85
|
+
gh pr view PR_NUMBER --json files,headRefName,baseRefName
|
|
98
86
|
```
|
|
99
|
-
|
|
87
|
+
|
|
88
|
+
**API:**
|
|
89
|
+
```bash
|
|
90
|
+
curl -s \
|
|
91
|
+
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
|
92
|
+
https://api.github.com/repos/OWNER/REPO/pulls/PR_NUMBER/files
|
|
100
93
|
```
|
|
101
94
|
|
|
102
|
-
|
|
95
|
+
### 2. 파일별 분석
|
|
103
96
|
|
|
104
|
-
|
|
97
|
+
선택된 검사 항목 범위에서 각 변경 파일 분석:
|
|
98
|
+
- 버그 가능성 (null check, 경계값, 예외 처리)
|
|
99
|
+
- 타입 오류 (TypeScript)
|
|
100
|
+
- 성능 이슈
|
|
101
|
+
- 컨벤션 위반
|
|
102
|
+
- 보안 취약점
|
|
105
103
|
|
|
106
|
-
|
|
104
|
+
### 3. 인라인 코멘트 작성 (라인별 개별 등록)
|
|
105
|
+
|
|
106
|
+
**핵심 원칙: 모든 지적사항은 해당 코드 라인에 직접 코멘트로 등록한다.**
|
|
107
|
+
|
|
108
|
+
코멘트 본문 형식:
|
|
109
|
+
```
|
|
110
|
+
{제목}
|
|
111
|
+
|
|
112
|
+
{설명}
|
|
107
113
|
|
|
114
|
+
```suggestion
|
|
115
|
+
{수정 제안 코드 (해당 라인 전체 대체)}
|
|
116
|
+
```
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- suggestion 블록은 수정 제안이 명확한 경우에만 포함
|
|
120
|
+
- 한 코멘트에 한 가지 지적사항만 담는다
|
|
121
|
+
- 검사 범위가 지정된 경우 첫 코멘트 또는 요약에 명시: `> 검사 범위: code, security`
|
|
122
|
+
|
|
123
|
+
#### 인라인 코멘트 등록 방법
|
|
124
|
+
|
|
125
|
+
**MCP:**
|
|
126
|
+
→ `create_review` 도구의 `comments` 배열에 각 항목 포함
|
|
127
|
+
|
|
128
|
+
**CLI (파일별 개별 등록):**
|
|
108
129
|
```bash
|
|
109
|
-
|
|
130
|
+
# diff의 position 값 또는 line 번호 사용
|
|
131
|
+
gh api repos/OWNER/REPO/pulls/PR_NUMBER/reviews \
|
|
132
|
+
--method POST \
|
|
133
|
+
--field body="" \
|
|
134
|
+
--field event="COMMENT" \
|
|
135
|
+
--field "comments[][path]=파일경로" \
|
|
136
|
+
--field "comments[][line]=라인번호" \
|
|
137
|
+
--field "comments[][body]=코멘트 본문"
|
|
110
138
|
```
|
|
111
139
|
|
|
112
|
-
|
|
140
|
+
여러 코멘트는 `comments[]` 배열에 모두 담아 **한 번의 review 요청**으로 제출한다.
|
|
141
|
+
|
|
142
|
+
**API:**
|
|
143
|
+
```bash
|
|
144
|
+
curl -s -X POST \
|
|
145
|
+
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
|
146
|
+
-H "Content-Type: application/json" \
|
|
147
|
+
https://api.github.com/repos/OWNER/REPO/pulls/PR_NUMBER/reviews \
|
|
148
|
+
-d '{
|
|
149
|
+
"body": "요약 코멘트",
|
|
150
|
+
"event": "COMMENT",
|
|
151
|
+
"comments": [
|
|
152
|
+
{
|
|
153
|
+
"path": "src/foo.js",
|
|
154
|
+
"line": 42,
|
|
155
|
+
"body": "코멘트 본문 (suggestion 포함 가능)"
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
}'
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`event` 값: 지적사항이 있으면 `"REQUEST_CHANGES"`, 없으면 `"APPROVE"`, 코멘트만이면 `"COMMENT"`
|
|
113
162
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
- 코딩 컨벤션 위반
|
|
137
|
-
- 테스트 커버리지 누락
|
|
138
|
-
|
|
139
|
-
3. 코멘트 작성 규칙 (템플릿 적용):
|
|
140
|
-
|
|
141
|
-
각 코멘트 형식:
|
|
142
|
-
```
|
|
143
|
-
{severityEmoji.SEVERITY} {title}
|
|
144
|
-
|
|
145
|
-
{description}
|
|
146
|
-
|
|
147
|
-
[includeCodeExample=true인 경우]
|
|
148
|
-
**Before:**
|
|
149
|
-
```lang
|
|
150
|
-
// 문제 코드
|
|
151
|
-
```
|
|
152
|
-
**After:**
|
|
153
|
-
```lang
|
|
154
|
-
// 개선 코드
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
{suggestion}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
리뷰 언어(`language`)에 맞게 작성. `ko`이면 한국어, `en`이면 영어.
|
|
161
|
-
|
|
162
|
-
4. 전체 요약 생성:
|
|
163
|
-
- 심각도별 건수 표
|
|
164
|
-
- 주요 발견사항
|
|
165
|
-
- 종합 의견
|
|
166
|
-
- 푸터: `review.footer`의 `{model}`을 실제 모델명으로, `{date}`를 현재 날짜로 치환
|
|
167
|
-
|
|
168
|
-
5. 리뷰 제출 (CRITICAL/HIGH 없으면 `APPROVE`, 있으면 `REQUEST_CHANGES`):
|
|
169
|
-
|
|
170
|
-
**MCP:**
|
|
171
|
-
→ GitHub MCP `create_review` 도구 사용
|
|
172
|
-
|
|
173
|
-
**CLI:**
|
|
174
|
-
```bash
|
|
175
|
-
gh pr review PR_NUMBER --approve --body "REVIEW_BODY"
|
|
176
|
-
# 또는
|
|
177
|
-
gh pr review PR_NUMBER --request-changes --body "REVIEW_BODY"
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
**API:**
|
|
181
|
-
```bash
|
|
182
|
-
curl -s -X POST \
|
|
183
|
-
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
|
184
|
-
-H "Content-Type: application/json" \
|
|
185
|
-
https://api.github.com/repos/OWNER/REPO/pulls/PR_NUMBER/reviews \
|
|
186
|
-
-d '{"body":"REVIEW_BODY","event":"APPROVE","comments":[]}'
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
6. 상태 업데이트:
|
|
190
|
-
```bash
|
|
191
|
-
node -e "import('./src/state.js').then(({writeState})=>writeState('review-agent',{status:'completed',progress:100,approved:APPROVED_BOOL,commentCount:COMMENT_COUNT}))"
|
|
192
|
-
```
|
|
163
|
+
### 4. 전체 요약 (PR 전체 코멘트)
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
## 리뷰 요약
|
|
167
|
+
|
|
168
|
+
> 검사 범위: {scope}
|
|
169
|
+
|
|
170
|
+
### 주요 발견사항
|
|
171
|
+
- ...
|
|
172
|
+
|
|
173
|
+
### 종합 의견
|
|
174
|
+
...
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
푸터: `review.footer`의 `{model}`을 실제 모델명으로, `{date}`를 현재 날짜로 치환.
|
|
178
|
+
기본 푸터: `{model} · {date}` (bylane 문구 없음)
|
|
179
|
+
|
|
180
|
+
### 5. 상태 업데이트
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
node -e "import('./src/state.js').then(({writeState})=>writeState('review-agent',{status:'completed',progress:100,approved:APPROVED_BOOL,commentCount:COMMENT_COUNT}))"
|
|
184
|
+
```
|
|
193
185
|
|
|
194
186
|
## 출력
|
|
195
187
|
|
|
@@ -199,8 +191,8 @@ node -e "import('./src/state.js').then(({writeState})=>writeState('review-agent'
|
|
|
199
191
|
"agent": "review-agent",
|
|
200
192
|
"status": "completed",
|
|
201
193
|
"progress": 100,
|
|
202
|
-
"approved":
|
|
203
|
-
"commentCount":
|
|
194
|
+
"approved": false,
|
|
195
|
+
"commentCount": 5
|
|
204
196
|
}
|
|
205
197
|
```
|
|
206
198
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -12,6 +12,11 @@ const args = process.argv.slice(2)
|
|
|
12
12
|
const command = args[0] || 'install'
|
|
13
13
|
const useSymlink = args.includes('--symlink')
|
|
14
14
|
|
|
15
|
+
// 사용자 설정 파일 — 절대 덮어쓰지 않는다
|
|
16
|
+
const USER_CONFIG_FILES = [
|
|
17
|
+
'.bylane/bylane.json',
|
|
18
|
+
]
|
|
19
|
+
|
|
15
20
|
const TARGETS = [
|
|
16
21
|
{ src: join(ROOT, 'commands'), dest: join(CLAUDE_DIR, 'commands'), label: 'Commands' },
|
|
17
22
|
{ src: join(ROOT, 'hooks'), dest: join(CLAUDE_DIR, 'hooks'), label: 'Hooks' },
|
|
@@ -31,7 +36,7 @@ function backupAndCopy(src, dest, file, label) {
|
|
|
31
36
|
const backupPath = `${destFile}.bak`
|
|
32
37
|
renameSync(destFile, backupPath)
|
|
33
38
|
copyFileSync(srcFile, destFile)
|
|
34
|
-
console.log(` ~ ${label}: ${file} (기존 파일 -> ${file}.bak)`)
|
|
39
|
+
console.log(` ~ ${label}: ${file} (업데이트됨, 기존 파일 -> ${file}.bak)`)
|
|
35
40
|
} else {
|
|
36
41
|
copyFileSync(srcFile, destFile)
|
|
37
42
|
console.log(` + ${label}: ${file}`)
|
|
@@ -48,41 +53,41 @@ function registerHooks() {
|
|
|
48
53
|
const hookScript = join(CLAUDE_DIR, 'hooks', 'bylane-agent-tracker.js')
|
|
49
54
|
settings.hooks = settings.hooks ?? {}
|
|
50
55
|
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
h.hooks?.some(hh => hh.command?.includes('bylane-agent-tracker'))
|
|
55
|
-
)
|
|
56
|
-
if (!preExists) {
|
|
57
|
-
settings.hooks.PreToolUse.push({
|
|
58
|
-
matcher: 'Agent',
|
|
59
|
-
hooks: [{ type: 'command', command: `node ${hookScript} pre` }]
|
|
60
|
-
})
|
|
61
|
-
console.log(' + Hook: PreToolUse/Agent → bylane-agent-tracker')
|
|
62
|
-
} else {
|
|
63
|
-
console.log(' = Hook: PreToolUse/Agent (이미 등록됨)')
|
|
64
|
-
}
|
|
56
|
+
// 기존 bylane 훅 제거 후 재등록 (버전 업 시 경로 변경 대응)
|
|
57
|
+
const stripBylane = (arr) =>
|
|
58
|
+
(arr ?? []).filter(h => !h.hooks?.some(hh => hh.command?.includes('bylane-agent-tracker')))
|
|
65
59
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
hooks: [{ type: 'command', command: `node ${hookScript} post` }]
|
|
75
|
-
})
|
|
76
|
-
console.log(' + Hook: PostToolUse/Agent → bylane-agent-tracker')
|
|
77
|
-
} else {
|
|
78
|
-
console.log(' = Hook: PostToolUse/Agent (이미 등록됨)')
|
|
79
|
-
}
|
|
60
|
+
settings.hooks.PreToolUse = [
|
|
61
|
+
...stripBylane(settings.hooks.PreToolUse),
|
|
62
|
+
{ matcher: 'Agent', hooks: [{ type: 'command', command: `node ${hookScript} pre` }] }
|
|
63
|
+
]
|
|
64
|
+
settings.hooks.PostToolUse = [
|
|
65
|
+
...stripBylane(settings.hooks.PostToolUse),
|
|
66
|
+
{ matcher: 'Agent', hooks: [{ type: 'command', command: `node ${hookScript} post` }] }
|
|
67
|
+
]
|
|
80
68
|
|
|
81
69
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2))
|
|
70
|
+
console.log(' ~ Hook: bylane-agent-tracker 등록 (최신 경로로 갱신)')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function preservedConfigs() {
|
|
74
|
+
const found = USER_CONFIG_FILES.filter(f => existsSync(f))
|
|
75
|
+
if (found.length > 0) {
|
|
76
|
+
console.log('\n [보존된 사용자 설정]')
|
|
77
|
+
found.forEach(f => console.log(` * ${f}`))
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isUpdateMode() {
|
|
82
|
+
return existsSync(join(CLAUDE_DIR, 'commands', 'bylane.md'))
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
function install() {
|
|
85
|
-
|
|
86
|
+
const updating = isUpdateMode()
|
|
87
|
+
console.log(updating
|
|
88
|
+
? '\n byLane 업데이트 중...\n'
|
|
89
|
+
: '\n byLane 설치 중...\n'
|
|
90
|
+
)
|
|
86
91
|
|
|
87
92
|
for (const { src, dest, label } of TARGETS) {
|
|
88
93
|
mkdirSync(dest, { recursive: true })
|
|
@@ -108,8 +113,17 @@ function install() {
|
|
|
108
113
|
|
|
109
114
|
console.log('')
|
|
110
115
|
registerHooks()
|
|
116
|
+
preservedConfigs()
|
|
111
117
|
|
|
112
|
-
|
|
118
|
+
if (updating) {
|
|
119
|
+
console.log(`
|
|
120
|
+
byLane 업데이트 완료!
|
|
121
|
+
|
|
122
|
+
사용자 설정(.bylane/bylane.json)은 그대로 유지됩니다.
|
|
123
|
+
Claude Code를 재시작하면 변경사항이 적용됩니다.
|
|
124
|
+
`)
|
|
125
|
+
} else {
|
|
126
|
+
console.log(`
|
|
113
127
|
byLane 설치 완료!
|
|
114
128
|
|
|
115
129
|
다음 단계:
|
|
@@ -122,12 +136,19 @@ function install() {
|
|
|
122
136
|
|
|
123
137
|
/bylane 다크모드 토글 추가해줘
|
|
124
138
|
`)
|
|
139
|
+
}
|
|
125
140
|
}
|
|
126
141
|
|
|
127
142
|
if (command === 'install') {
|
|
128
143
|
install()
|
|
144
|
+
} else if (command === 'monitor') {
|
|
145
|
+
// 항상 현재 패키지의 모니터 실행 (버전 일치 보장)
|
|
146
|
+
const monitorPath = join(__dirname, 'monitor', 'index.js')
|
|
147
|
+
const { spawn } = await import('child_process')
|
|
148
|
+
const child = spawn(process.execPath, [monitorPath], { stdio: 'inherit' })
|
|
149
|
+
child.on('exit', code => process.exit(code ?? 0))
|
|
129
150
|
} else {
|
|
130
151
|
console.error(`알 수 없는 명령: ${command}`)
|
|
131
|
-
console.error('사용법: npx bylane [install] [--symlink]')
|
|
152
|
+
console.error('사용법: npx @elyun/bylane [install|monitor] [--symlink]')
|
|
132
153
|
process.exit(1)
|
|
133
154
|
}
|
package/src/monitor/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import blessed from 'blessed'
|
|
2
3
|
import { createLayout } from './layout.js'
|
|
3
4
|
import { createPoller } from './poller.js'
|
|
4
5
|
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'fs'
|
|
@@ -9,6 +10,7 @@ const poller = createPoller()
|
|
|
9
10
|
|
|
10
11
|
const startTime = Date.now()
|
|
11
12
|
const STATE_DIR = '.bylane/state'
|
|
13
|
+
let lastStates = {}
|
|
12
14
|
const SUBAGENTS_FILE = join(STATE_DIR, 'subagents.json')
|
|
13
15
|
const CANCEL_FILE = join(STATE_DIR, 'cancel.json')
|
|
14
16
|
|
|
@@ -20,6 +22,7 @@ function readSubagents() {
|
|
|
20
22
|
onCleanup(() => poller.stop())
|
|
21
23
|
|
|
22
24
|
poller.onChange((states) => {
|
|
25
|
+
lastStates = states
|
|
23
26
|
const active = Object.values(states).find(s => s.status === 'in_progress')
|
|
24
27
|
const workflowTitle = active ? (active.currentTask ?? active.agent) : 'Idle'
|
|
25
28
|
|
|
@@ -34,8 +37,71 @@ poller.onChange((states) => {
|
|
|
34
37
|
status.update()
|
|
35
38
|
})
|
|
36
39
|
|
|
40
|
+
// 's' — 실행 중인 루프 선택 종료
|
|
41
|
+
screen.key('s', () => {
|
|
42
|
+
const runningLoops = Object.entries(lastStates)
|
|
43
|
+
.filter(([name, s]) => name.endsWith('-loop') && s?.status === 'running' && s?.pid)
|
|
44
|
+
.map(([name, s]) => ({ name, pid: s.pid }))
|
|
45
|
+
|
|
46
|
+
if (runningLoops.length === 0) {
|
|
47
|
+
header.update({
|
|
48
|
+
workflowTitle: '실행 중인 루프 없음',
|
|
49
|
+
time: new Date().toLocaleTimeString('ko-KR', { hour12: false }),
|
|
50
|
+
elapsed: `${Math.floor((Date.now() - startTime) / 1000)}s`
|
|
51
|
+
})
|
|
52
|
+
screen.render()
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// blessed list 오버레이
|
|
57
|
+
const list = blessed.list({
|
|
58
|
+
top: 'center',
|
|
59
|
+
left: 'center',
|
|
60
|
+
width: 40,
|
|
61
|
+
height: runningLoops.length + 4,
|
|
62
|
+
label: ' 종료할 루프 선택 (Enter/Esc) ',
|
|
63
|
+
tags: true,
|
|
64
|
+
border: { type: 'line' },
|
|
65
|
+
style: {
|
|
66
|
+
border: { fg: 'yellow' },
|
|
67
|
+
selected: { bg: 'blue', fg: 'white' },
|
|
68
|
+
item: { fg: 'white' }
|
|
69
|
+
},
|
|
70
|
+
keys: true,
|
|
71
|
+
items: runningLoops.map(l => ` ${l.name} (PID: ${l.pid})`)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
screen.append(list)
|
|
75
|
+
list.focus()
|
|
76
|
+
screen.render()
|
|
77
|
+
|
|
78
|
+
list.on('select', (item, idx) => {
|
|
79
|
+
const loop = runningLoops[idx]
|
|
80
|
+
try {
|
|
81
|
+
process.kill(loop.pid, 'SIGTERM')
|
|
82
|
+
header.update({
|
|
83
|
+
workflowTitle: `${loop.name} 종료 요청됨 (PID: ${loop.pid})`,
|
|
84
|
+
time: new Date().toLocaleTimeString('ko-KR', { hour12: false }),
|
|
85
|
+
elapsed: `${Math.floor((Date.now() - startTime) / 1000)}s`
|
|
86
|
+
})
|
|
87
|
+
} catch {
|
|
88
|
+
header.update({
|
|
89
|
+
workflowTitle: `${loop.name} 종료 실패 (이미 종료됨?)`,
|
|
90
|
+
time: new Date().toLocaleTimeString('ko-KR', { hour12: false }),
|
|
91
|
+
elapsed: `${Math.floor((Date.now() - startTime) / 1000)}s`
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
screen.remove(list)
|
|
95
|
+
screen.render()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
list.key(['escape', 'q'], () => {
|
|
99
|
+
screen.remove(list)
|
|
100
|
+
screen.render()
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
37
104
|
// 'c' — 하위 에이전트 취소 플래그 토글
|
|
38
|
-
const cancelLabel = () => existsSync(CANCEL_FILE) ? '[취소 활성]' : ''
|
|
39
105
|
screen.key('c', () => {
|
|
40
106
|
if (existsSync(CANCEL_FILE)) {
|
|
41
107
|
unlinkSync(CANCEL_FILE)
|
package/src/monitor/layout.js
CHANGED
|
@@ -9,9 +9,48 @@ export function createLayout() {
|
|
|
9
9
|
const screen = blessed.screen({
|
|
10
10
|
smartCSR: true,
|
|
11
11
|
fullUnicode: true,
|
|
12
|
+
dockBorders: true,
|
|
12
13
|
title: 'byLane Monitor'
|
|
13
14
|
})
|
|
14
15
|
|
|
16
|
+
// 최소 크기 경고 오버레이
|
|
17
|
+
const tooSmall = blessed.box({
|
|
18
|
+
top: 'center',
|
|
19
|
+
left: 'center',
|
|
20
|
+
width: 40,
|
|
21
|
+
height: 5,
|
|
22
|
+
content: '\n 터미널이 너무 작습니다.\n 최소 80×24 이상으로 키워주세요.',
|
|
23
|
+
align: 'center',
|
|
24
|
+
tags: true,
|
|
25
|
+
border: { type: 'line' },
|
|
26
|
+
style: { fg: 'white', bg: 'red', border: { fg: 'white' } },
|
|
27
|
+
hidden: true
|
|
28
|
+
})
|
|
29
|
+
screen.append(tooSmall)
|
|
30
|
+
|
|
31
|
+
function checkSize() {
|
|
32
|
+
const cols = screen.width
|
|
33
|
+
const rows = screen.height
|
|
34
|
+
if (cols < 80 || rows < 24) {
|
|
35
|
+
tooSmall.show()
|
|
36
|
+
} else {
|
|
37
|
+
tooSmall.hide()
|
|
38
|
+
}
|
|
39
|
+
screen.render()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
screen.on('resize', () => {
|
|
43
|
+
// 약간의 지연으로 tmux/pane 리사이즈 완료 후 렌더링
|
|
44
|
+
setTimeout(() => {
|
|
45
|
+
screen.alloc()
|
|
46
|
+
screen.render()
|
|
47
|
+
checkSize()
|
|
48
|
+
}, 50)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// 초기 크기 체크
|
|
52
|
+
process.nextTick(checkSize)
|
|
53
|
+
|
|
15
54
|
const header = createHeader(screen)
|
|
16
55
|
const pipeline = createPipelinePanel(screen)
|
|
17
56
|
const log = createLogPanel(screen)
|
|
@@ -23,7 +62,7 @@ export function createLayout() {
|
|
|
23
62
|
left: 0,
|
|
24
63
|
width: '100%',
|
|
25
64
|
height: 1,
|
|
26
|
-
content: ' [q]종료 [c]에이전트취소토글 [Tab]포커스 [j/k]로그스크롤',
|
|
65
|
+
content: ' [q]종료 [c]에이전트취소토글 [s]루프종료 [Tab]포커스 [j/k]로그스크롤',
|
|
27
66
|
style: { fg: 'black', bg: 'cyan' }
|
|
28
67
|
})
|
|
29
68
|
screen.append(footer)
|
package/src/respond-loop.js
CHANGED
|
@@ -186,5 +186,5 @@ process.on('SIGTERM', () => {
|
|
|
186
186
|
process.exit(0)
|
|
187
187
|
})
|
|
188
188
|
|
|
189
|
-
writeState('respond-loop', { status: 'running', startedAt: new Date().toISOString() }, STATE_DIR)
|
|
189
|
+
writeState('respond-loop', { status: 'running', startedAt: new Date().toISOString(), pid: process.pid }, STATE_DIR)
|
|
190
190
|
console.log('respond-loop 시작. Ctrl+C로 종료.')
|
package/src/review-loop.js
CHANGED
|
@@ -133,5 +133,5 @@ process.on('SIGTERM', () => {
|
|
|
133
133
|
process.exit(0)
|
|
134
134
|
})
|
|
135
135
|
|
|
136
|
-
writeState('review-loop', { status: 'running', startedAt: new Date().toISOString() }, STATE_DIR)
|
|
136
|
+
writeState('review-loop', { status: 'running', startedAt: new Date().toISOString(), pid: process.pid }, STATE_DIR)
|
|
137
137
|
console.log('review-loop 시작. Ctrl+C로 종료.')
|
|
@@ -1,86 +1,56 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 리뷰 템플릿
|
|
2
2
|
|
|
3
3
|
이 파일을 복사해 프로젝트별로 커스터마이즈하세요.
|
|
4
4
|
`.bylane/bylane.json`의 `review.templateFile`에 경로를 지정하면 적용됩니다.
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
## 코멘트 형식
|
|
8
|
+
## 인라인 코멘트 형식
|
|
9
9
|
|
|
10
|
-
각 리뷰 코멘트는
|
|
10
|
+
각 리뷰 코멘트는 **해당 코드 라인에 직접** 작성합니다.
|
|
11
11
|
|
|
12
12
|
```
|
|
13
|
-
{
|
|
13
|
+
{title}
|
|
14
14
|
|
|
15
15
|
{description}
|
|
16
16
|
|
|
17
|
-
{
|
|
18
|
-
|
|
19
|
-
{suggestion}
|
|
17
|
+
{suggestion_block}
|
|
20
18
|
```
|
|
21
19
|
|
|
22
20
|
### 필드 설명
|
|
23
21
|
|
|
24
|
-
- `{
|
|
25
|
-
- `{title}` — 문제 제목 (한 줄)
|
|
22
|
+
- `{title}` — 문제 제목 (한 줄, 간결하게)
|
|
26
23
|
- `{description}` — 문제 설명 및 이유
|
|
27
|
-
- `{
|
|
28
|
-
- `{suggestion}` — 수정 방법 또는 개선 예시 코드
|
|
29
|
-
|
|
30
|
-
---
|
|
24
|
+
- `{suggestion_block}` — GitHub suggestion 블록 (수정 제안 코드)
|
|
31
25
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
| 심각도 | 기준 | 처리 |
|
|
35
|
-
|--------|------|------|
|
|
36
|
-
| CRITICAL | 버그, 보안 취약점, 데이터 손실 가능성 | 즉시 수정 필요, PR 차단 |
|
|
37
|
-
| HIGH | 잘못된 로직, 성능 심각 저하 | 수정 강력 권장 |
|
|
38
|
-
| MEDIUM | 코드 품질, 가독성, 테스트 누락 | 개선 권장 |
|
|
39
|
-
| LOW | 네이밍, 스타일, 선택적 개선 | 참고용 |
|
|
40
|
-
|
|
41
|
-
---
|
|
26
|
+
### suggestion 블록 형식
|
|
42
27
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
```language
|
|
47
|
-
// 문제가 있는 코드
|
|
28
|
+
````
|
|
29
|
+
```suggestion
|
|
30
|
+
// 수정된 코드 (원본 라인을 그대로 대체)
|
|
48
31
|
```
|
|
32
|
+
````
|
|
49
33
|
|
|
50
|
-
|
|
51
|
-
```language
|
|
52
|
-
// 개선된 코드
|
|
53
|
-
```
|
|
34
|
+
GitHub에서 "Apply suggestion" 버튼으로 바로 적용 가능합니다.
|
|
54
35
|
|
|
55
36
|
---
|
|
56
37
|
|
|
57
|
-
## 전체
|
|
38
|
+
## 전체 요약 형식 (PR 전체 코멘트)
|
|
58
39
|
|
|
59
40
|
```
|
|
60
|
-
##
|
|
61
|
-
|
|
62
|
-
| 심각도 | 건수 |
|
|
63
|
-
|--------|------|
|
|
64
|
-
| CRITICAL | N |
|
|
65
|
-
| HIGH | N |
|
|
66
|
-
| MEDIUM | N |
|
|
67
|
-
| LOW | N |
|
|
41
|
+
## 리뷰 요약
|
|
68
42
|
|
|
69
43
|
### 주요 발견사항
|
|
70
44
|
- ...
|
|
71
45
|
|
|
72
46
|
### 종합 의견
|
|
73
47
|
...
|
|
74
|
-
|
|
75
|
-
---
|
|
76
|
-
{footer}
|
|
77
48
|
```
|
|
78
49
|
|
|
79
50
|
---
|
|
80
51
|
|
|
81
52
|
## 푸터
|
|
82
53
|
|
|
83
|
-
기본값: `
|
|
54
|
+
기본값: `{model} · {date}`
|
|
84
55
|
|
|
85
|
-
`{model}` — 실제
|
|
86
|
-
커스터마이즈 예시: `AI 리뷰 by byLane (claude-sonnet-4-6) · {date}`
|
|
56
|
+
`{model}`, `{date}` — 실제 모델명과 날짜로 자동 치환됩니다.
|