@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.
@@ -19,14 +19,11 @@ Claude가 직접 실행하지 않는다. 사용자에게 아래 명령을 안내
19
19
  모니터 대시보드는 터미널에서 직접 실행하세요:
20
20
 
21
21
  ```bash
22
- npm run monitor
22
+ npx @elyun/bylane monitor
23
23
  ```
24
24
 
25
- byLane이 설치된 디렉토리에서 실행하거나, 현재 프로젝트에서:
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
- ## GitHub 리뷰 대응 템플릿 탐지
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
- - `REVIEW_RESPONSE_TEMPLATE.md`가 있으면 답글 형식을 **반드시** 따른다.
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. 코드 수정 (code-agent 서브 실행)
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
- 1. 코멘트에 근거를 기술한 반박 답글 작성:
122
- - 의도적 설계 결정: 배경 설명
123
- - 성능 트레이드오프: 구체적 수치 근거
124
- - 스펙 요구사항과 일치하는 경우: 이슈 링크 첨부
125
- 2. 위와 동일한 방법으로 답글 게시
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": "accept",
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` → accept/rebut 선택 프롬프트 표시
134
+ `/bylane respond #45`
@@ -63,7 +63,7 @@ import('./src/state.js').then(({readState, writeState}) => {
63
63
  "
64
64
  ```
65
65
 
66
- 4. 다음 pending 항목으로 반복. pending 없으면 30초 대기 후 재확인.
66
+ 4. 다음 pending 항목으로 반복. pending 없으면 5분 대기 후 재확인 (폴러 주기와 동일).
67
67
 
68
68
  ## 큐 항목 스키마
69
69
 
@@ -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
- 없으면 아래 bylane 설정의 템플릿을 사용한다.
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
- > 검사 범위: code, security
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
- PR 번호 (`.bylane/state/pr-agent.json`에서 자동 로드, 또는 수동 전달)
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
- node -e "import('./src/state.js').then(({writeState})=>writeState('review-agent',{status:'in_progress',startedAt:new Date().toISOString(),progress:0,retries:0,log:[]}))"
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
- 1. PR diff 로드:
115
-
116
- **MCP:**
117
- GitHub MCP `get_pull_request_files` 도구 사용
118
-
119
- **CLI:**
120
- ```bash
121
- gh pr diff PR_NUMBER
122
- gh pr view PR_NUMBER --json files
123
- ```
124
-
125
- **API:**
126
- ```bash
127
- curl -s \
128
- -H "Authorization: Bearer $GITHUB_TOKEN" \
129
- https://api.github.com/repos/OWNER/REPO/pulls/PR_NUMBER/files
130
- ```
131
-
132
- 2. 변경된 파일별 분석:
133
- - 버그 가능성 (null check, 경계값 등)
134
- - 타입 오류 (TypeScript)
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": true,
203
- "commentCount": 3
194
+ "approved": false,
195
+ "commentCount": 5
204
196
  }
205
197
  ```
206
198
 
@@ -74,7 +74,7 @@ import('./src/state.js').then(({readState, writeState}) => {
74
74
  ```
75
75
 
76
76
  3. 다음 pending 항목으로 반복
77
- 4. pending 없으면 30초 대기 후 재확인
77
+ 4. pending 없으면 5분 대기 후 재확인 (폴러 주기와 동일)
78
78
 
79
79
  ## 큐 항목 스키마
80
80
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elyun/bylane",
3
- "version": "1.15.0",
3
+ "version": "1.17.0",
4
4
  "description": "Frontend development harness for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
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
- // PreToolUse
52
- settings.hooks.PreToolUse = settings.hooks.PreToolUse ?? []
53
- const preExists = settings.hooks.PreToolUse.some(h =>
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
- // PostToolUse
67
- settings.hooks.PostToolUse = settings.hooks.PostToolUse ?? []
68
- const postExists = settings.hooks.PostToolUse.some(h =>
69
- h.hooks?.some(hh => hh.command?.includes('bylane-agent-tracker'))
70
- )
71
- if (!postExists) {
72
- settings.hooks.PostToolUse.push({
73
- matcher: 'Agent',
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
- console.log('\n byLane 설치 중...\n')
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
- console.log(`
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
  }
@@ -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)
@@ -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)
@@ -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로 종료.')
@@ -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
- # byLane 리뷰 템플릿
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
- {severity} {title}
13
+ {title}
14
14
 
15
15
  {description}
16
16
 
17
- {code_example}
18
-
19
- {suggestion}
17
+ {suggestion_block}
20
18
  ```
21
19
 
22
20
  ### 필드 설명
23
21
 
24
- - `{severity}` — 심각도 레이블 (`[CRITICAL]` / `[HIGH]` / `[MEDIUM]` / `[LOW]`)
25
- - `{title}` — 문제 제목 (한 줄)
22
+ - `{title}` — 문제 제목 ( 줄, 간결하게)
26
23
  - `{description}` — 문제 설명 및 이유
27
- - `{code_example}` — 문제가 있는 코드 예시 (선택, `review.includeCodeExample: false`로 비활성)
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
- **Before (문제 코드):**
46
- ```language
47
- // 문제가 있는 코드
28
+ ````
29
+ ```suggestion
30
+ // 수정된 코드 (원본 라인을 그대로 대체)
48
31
  ```
32
+ ````
49
33
 
50
- **After (수정 제안):**
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
- 기본값: `Reviewed by byLane · model: {model}`
54
+ 기본값: `{model} · {date}`
84
55
 
85
- `{model}` — 실제 사용된 모델명으로 자동 치환됩니다.
86
- 커스터마이즈 예시: `AI 리뷰 by byLane (claude-sonnet-4-6) · {date}`
56
+ `{model}`, `{date}` — 실제 모델명과 날짜로 자동 치환됩니다.