@elyun/bylane 1.33.0 → 1.34.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.
@@ -18,17 +18,25 @@ description: PR의 diff를 파일별로 분석하여 코드 라인별 인라인
18
18
 
19
19
  ## GitHub 리뷰 템플릿 탐지
20
20
 
21
- bylane 설정보다 먼저 프로젝트 GitHub 리뷰 템플릿을 탐색한다:
21
+ 다음 순서로 탐색한다. 먼저 발견된 파일을 **최우선**으로 따른다:
22
22
 
23
23
  ```bash
24
+ # 1순위: 프로젝트 .github/ 커스텀 템플릿
24
25
  ls .github/REVIEW_TEMPLATE.md \
25
26
  .github/review_template.md \
26
27
  .github/CODE_REVIEW_TEMPLATE.md \
27
28
  docs/REVIEW_TEMPLATE.md 2>/dev/null | head -1
29
+
30
+ # 2순위: 프로젝트 .bylane/ 설치 템플릿 (bylane setup 시 설치됨)
31
+ # .bylane/templates/review-template.md
32
+
33
+ # 3순위: 글로벌 fallback (~/.claude/templates/bylane/review-template.md)
28
34
  ```
29
35
 
30
- GitHub 템플릿이 있으면 해당 형식을 **최우선**으로 따른다.
31
- 없으면 `templates/review-template.md`를 사용한다.
36
+ 우선순위 요약:
37
+ 1. `.github/` 내 커스텀 템플릿 (프로젝트별 재정의)
38
+ 2. `.bylane/templates/review-template.md` (프로젝트 기본값)
39
+ 3. `~/.claude/templates/bylane/review-template.md` (글로벌 기본값)
32
40
 
33
41
  ## 검사 항목 선택
34
42
 
@@ -10,9 +10,10 @@ ALWAYS complete all 8 steps before saving. NEVER skip steps.
10
10
 
11
11
  ## 실행 전 준비
12
12
 
13
- 1. `.bylane/` 디렉토리가 없으면 생성:
13
+ 1. `.bylane/` 디렉토리가 없으면 생성하고 기본 템플릿을 설치:
14
14
  ```bash
15
15
  mkdir -p .bylane/state
16
+ npx @elyun/bylane templates install
16
17
  ```
17
18
 
18
19
  2. 기존 bylane.json 있으면 현재 설정을 로드해 기본값으로 사용.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elyun/bylane",
3
- "version": "1.33.0",
3
+ "version": "1.34.0",
4
4
  "description": "Frontend development harness for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -18,8 +18,9 @@ const USER_CONFIG_FILES = [
18
18
  ]
19
19
 
20
20
  const TARGETS = [
21
- { src: join(ROOT, 'commands'), dest: join(CLAUDE_DIR, 'commands'), label: 'Commands' },
22
- { src: join(ROOT, 'hooks'), dest: join(CLAUDE_DIR, 'hooks'), label: 'Hooks' },
21
+ { src: join(ROOT, 'commands'), dest: join(CLAUDE_DIR, 'commands'), label: 'Commands' },
22
+ { src: join(ROOT, 'hooks'), dest: join(CLAUDE_DIR, 'hooks'), label: 'Hooks' },
23
+ { src: join(ROOT, 'templates'), dest: join(CLAUDE_DIR, 'templates', 'bylane'), label: 'Templates' },
23
24
  ]
24
25
 
25
26
  function backupAndCopy(src, dest, file, label) {
@@ -369,6 +370,35 @@ if (command === 'install') {
369
370
  console.error('사용법: bylane loop <start|stop|status>')
370
371
  process.exit(1)
371
372
  }
373
+ } else if (command === 'templates') {
374
+ const subCmd = args[1] || 'install'
375
+ if (subCmd === 'install') {
376
+ const globalTemplatesDir = join(CLAUDE_DIR, 'templates', 'bylane')
377
+ const projectTemplatesDir = join('.bylane', 'templates')
378
+ if (!existsSync(globalTemplatesDir)) {
379
+ console.error(' 글로벌 템플릿이 없습니다. 먼저 npx @elyun/bylane install 을 실행하세요.')
380
+ process.exit(1)
381
+ }
382
+ mkdirSync(projectTemplatesDir, { recursive: true })
383
+ const files = readdirSync(globalTemplatesDir)
384
+ let copied = 0
385
+ for (const file of files) {
386
+ const dest = join(projectTemplatesDir, file)
387
+ if (!existsSync(dest)) {
388
+ copyFileSync(join(globalTemplatesDir, file), dest)
389
+ console.log(` + Templates: ${file}`)
390
+ copied++
391
+ } else {
392
+ console.log(` = Templates: ${file} (이미 존재, 건너뜀)`)
393
+ }
394
+ }
395
+ console.log(copied > 0
396
+ ? `\n 템플릿 ${copied}개가 .bylane/templates/ 에 설치되었습니다.`
397
+ : '\n 모든 템플릿이 이미 설치되어 있습니다.')
398
+ } else {
399
+ console.error('사용법: bylane templates install')
400
+ process.exit(1)
401
+ }
372
402
  } else if (command === 'monitor') {
373
403
  // 항상 현재 패키지의 모니터 실행 (버전 일치 보장)
374
404
  const monitorPath = join(__dirname, 'monitor', 'index.js')
@@ -377,6 +407,6 @@ if (command === 'install') {
377
407
  child.on('exit', code => process.exit(code ?? 0))
378
408
  } else {
379
409
  console.error(`알 수 없는 명령: ${command}`)
380
- console.error('사용법: npx @elyun/bylane [install|uninstall|loop|monitor|preflight|state|memory|cleanup] [--symlink]')
410
+ console.error('사용법: npx @elyun/bylane [install|uninstall|loop|monitor|preflight|state|memory|cleanup|templates] [--symlink]')
381
411
  process.exit(1)
382
412
  }
package/src/config.js CHANGED
@@ -72,7 +72,7 @@ export const DEFAULT_CONFIG = {
72
72
  includeModel: true,
73
73
  includeCodeExample: true,
74
74
  autoApprove: false,
75
- templateFile: '',
75
+ templateFile: '.bylane/templates/review-template.md',
76
76
  severityEmoji: {
77
77
  CRITICAL: '[CRITICAL]',
78
78
  HIGH: '[HIGH]',
package/src/loop-utils.js CHANGED
@@ -97,11 +97,64 @@ export function isTmuxSessionAlive(sessionName) {
97
97
  }
98
98
  }
99
99
 
100
+ /**
101
+ * tmux 세션의 두 루프가 정상 기동됐는지 검증한다.
102
+ * state 파일에 running 상태가 기록될 때까지 최대 timeoutMs 대기.
103
+ * @param {string} sessionName
104
+ * @param {string} stateDir
105
+ * @param {number} timeoutMs
106
+ * @returns {{ ok: boolean, review: object, respond: object }}
107
+ */
108
+ export function verifyTmuxLoops(sessionName = 'bylane-loops', stateDir = '.bylane/state', timeoutMs = 8000) {
109
+ const loops = ['review-loop', 'respond-loop']
110
+ const result = {}
111
+
112
+ // 1. tmux 세션 및 윈도우 존재 확인
113
+ if (!isTmuxSessionAlive(sessionName)) {
114
+ return { ok: false, review: { alive: false }, respond: { alive: false } }
115
+ }
116
+
117
+ let windows = []
118
+ try {
119
+ const out = execSync(
120
+ `tmux list-windows -t ${sessionName} -F '#{window_name}'`,
121
+ { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
122
+ ).trim()
123
+ windows = out.split('\n').map(w => w.trim())
124
+ } catch { /* 세션이 막 종료됐을 수 있음 */ }
125
+
126
+ // 2. 각 루프의 state 파일이 running 상태가 될 때까지 폴링
127
+ const deadline = Date.now() + timeoutMs
128
+ const pending = new Set(loops)
129
+
130
+ while (pending.size > 0 && Date.now() < deadline) {
131
+ for (const loop of [...pending]) {
132
+ const state = readState(loop, stateDir)
133
+ if (state?.status === 'running' && state?.pid) {
134
+ result[loop] = { alive: true, pid: state.pid, startedAt: state.startedAt }
135
+ pending.delete(loop)
136
+ }
137
+ }
138
+ if (pending.size > 0) execSync('sleep 0.5')
139
+ }
140
+
141
+ // 타임아웃된 루프는 실패로 처리
142
+ for (const loop of pending) {
143
+ const windowName = loop.replace('-loop', '')
144
+ const windowAlive = windows.includes(windowName)
145
+ result[loop] = { alive: false, windowAlive }
146
+ }
147
+
148
+ const ok = loops.every(l => result[l]?.alive)
149
+ return { ok, review: result['review-loop'], respond: result['respond-loop'] }
150
+ }
151
+
100
152
  /**
101
153
  * tmux 세션에서 review-loop + respond-loop을 실행
102
154
  * @param {string} sessionName
155
+ * @param {string} stateDir
103
156
  */
104
- export function startTmuxLoops(sessionName = 'bylane-loops') {
157
+ export function startTmuxLoops(sessionName = 'bylane-loops', stateDir = '.bylane/state') {
105
158
  if (isTmuxSessionAlive(sessionName)) {
106
159
  console.log(`[tmux] 세션 '${sessionName}'이 이미 실행 중입니다.`)
107
160
  return { started: false, reason: 'already_running' }
@@ -118,8 +171,21 @@ export function startTmuxLoops(sessionName = 'bylane-loops') {
118
171
  { stdio: 'inherit' }
119
172
  )
120
173
 
121
- console.log(`[tmux] 세션 '${sessionName}' 시작 완료 (review-loop + respond-loop)`)
122
- return { started: true, sessionName }
174
+ console.log(`[tmux] 세션 '${sessionName}' 시작 루프 기동 확인 중...`)
175
+ const verification = verifyTmuxLoops(sessionName, stateDir)
176
+
177
+ if (verification.ok) {
178
+ console.log(`[tmux] review-loop (PID: ${verification.review.pid}) ✓`)
179
+ console.log(`[tmux] respond-loop (PID: ${verification.respond.pid}) ✓`)
180
+ } else {
181
+ const reviewStatus = verification.review?.alive ? `✓ PID: ${verification.review.pid}` : `✗ 기동 실패 (창: ${verification.review?.windowAlive ? '있음' : '없음'})`
182
+ const respondStatus = verification.respond?.alive ? `✓ PID: ${verification.respond.pid}` : `✗ 기동 실패 (창: ${verification.respond?.windowAlive ? '있음' : '없음'})`
183
+ console.log(`[tmux] review-loop ${reviewStatus}`)
184
+ console.log(`[tmux] respond-loop ${respondStatus}`)
185
+ console.log(`[tmux] 일부 루프가 정상 기동되지 않았습니다. tmux attach -t ${sessionName} 으로 확인하세요.`)
186
+ }
187
+
188
+ return { started: true, sessionName, verified: verification.ok, verification }
123
189
  }
124
190
 
125
191
  /**
@@ -174,9 +240,10 @@ export function resolveLoopMode() {
174
240
  if (mode === 'tmux') {
175
241
  try {
176
242
  execSync('which tmux', { stdio: ['pipe', 'pipe', 'pipe'] })
243
+ execSync('tmux -V', { stdio: ['pipe', 'pipe', 'pipe'] })
177
244
  return 'tmux'
178
245
  } catch {
179
- console.log('[loop] tmux 미설치 — process 모드로 fallback합니다.')
246
+ console.log('[loop] tmux 사용할 수 없음 — process 모드로 fallback합니다.')
180
247
  return 'process'
181
248
  }
182
249
  }
@@ -36,7 +36,7 @@ poller.onChange((states) => {
36
36
  pipeline.update(states, readSubagents())
37
37
  log.update(states)
38
38
  queue.update()
39
- status.update()
39
+ status.update(lastStates)
40
40
  })
41
41
 
42
42
  // 's' — 실행 중인 루프/에이전트 선택 종료
@@ -1,6 +1,14 @@
1
1
  import blessed from 'blessed'
2
+ import { execSync } from 'child_process'
2
3
  import { loadConfig } from '../../config.js'
3
4
 
5
+ function isTmuxAlive(sessionName) {
6
+ try {
7
+ execSync(`tmux has-session -t ${sessionName}`, { stdio: ['pipe', 'pipe', 'pipe'] })
8
+ return true
9
+ } catch { return false }
10
+ }
11
+
4
12
  export function createStatusPanel(screen) {
5
13
  const box = blessed.box({
6
14
  top: '60%',
@@ -15,13 +23,35 @@ export function createStatusPanel(screen) {
15
23
  screen.append(box)
16
24
 
17
25
  return {
18
- update() {
26
+ update(states = {}) {
19
27
  const config = loadConfig()
20
28
  const check = (v) => v ? '{green-fg}OK{/green-fg}' : '{red-fg}--{/red-fg}'
29
+
30
+ // 루프 상태
31
+ const reviewState = states['review-loop']
32
+ const respondState = states['respond-loop']
33
+ const loopMode = config.loop?.mode ?? 'tmux'
34
+ const sessionName = config.loop?.sessionName ?? 'bylane-loops'
35
+ const tmuxAlive = loopMode === 'tmux' ? isTmuxAlive(sessionName) : null
36
+
37
+ const loopStatus = (state) => {
38
+ if (!state) return '{red-fg}미실행{/red-fg}'
39
+ if (state.status === 'running') return `{green-fg}실행중{/green-fg} PID:${state.pid ?? '?'}`
40
+ if (state.status === 'stopped') return '{yellow-fg}중지됨{/yellow-fg}'
41
+ return `{grey-fg}${state.status ?? '알 수 없음'}{/grey-fg}`
42
+ }
43
+
21
44
  const lines = [
45
+ ` ── 루프 ──`,
46
+ ` review-loop ${loopStatus(reviewState)}`,
47
+ ` respond-loop ${loopStatus(respondState)}`,
48
+ tmuxAlive !== null
49
+ ? ` tmux [${sessionName}] ${tmuxAlive ? '{green-fg}세션 유지{/green-fg}' : '{red-fg}세션 없음{/red-fg}'}`
50
+ : ` 모드: process`,
51
+ ``,
52
+ ` ── 연동 ──`,
22
53
  ` GitHub ${check(true)} 연결됨`,
23
54
  ` Linear ${check(config.trackers.linear.enabled)} ${config.trackers.linear.enabled ? '활성' : '비활성'}`,
24
- ` Figma MCP ${check(config.extensions.figma.enabled)} ${config.extensions.figma.enabled ? '활성' : '비활성'}`,
25
55
  ` Slack ${check(config.notifications.slack.enabled)} ${config.notifications.slack.channel || '미설정'}`,
26
56
  ` Telegram ${check(config.notifications.telegram.enabled)} ${config.notifications.telegram.chatId || '미설정'}`,
27
57
  ``,
package/src/preflight.js CHANGED
@@ -20,7 +20,18 @@ function run(cmd) {
20
20
  export function checkTmux() {
21
21
  const which = run('which tmux')
22
22
  if (!which.ok) return { ok: false, reason: 'tmux 미설치 — loop을 process 모드로 실행합니다', fix: 'brew install tmux # 또는 https://github.com/tmux/tmux' }
23
- return { ok: true, detail: which.out }
23
+
24
+ const version = run('tmux -V')
25
+ if (!version.ok) return { ok: false, reason: 'tmux 바이너리가 있으나 실행 불가', fix: 'tmux 재설치 또는 PATH 확인' }
26
+
27
+ // 실제 세션 생성/삭제로 동작 검증
28
+ const testSession = `bylane-preflight-test-${Date.now()}`
29
+ const create = run(`tmux new-session -d -s ${testSession}`)
30
+ if (!create.ok) return { ok: false, reason: `tmux 세션 생성 실패: ${create.out || '알 수 없는 오류'}`, fix: 'tmux 서버 상태 확인: tmux kill-server && tmux new-session -d -s test' }
31
+
32
+ run(`tmux kill-session -t ${testSession}`)
33
+
34
+ return { ok: true, detail: version.out }
24
35
  }
25
36
 
26
37
  function checkGhCli() {