@elyun/bylane 1.18.0 → 1.20.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/README.md +28 -0
- package/commands/bylane-orchestrator.md +7 -1
- package/commands/bylane.md +20 -0
- package/package.json +1 -1
- package/src/cli.js +5 -0
- package/src/loop-utils.js +47 -0
- package/src/monitor/panels/pipeline.js +19 -4
- package/src/preflight.js +146 -0
- package/src/respond-loop.js +2 -0
- package/src/review-loop.js +2 -0
package/README.md
CHANGED
|
@@ -69,6 +69,34 @@ node src/cli.js install
|
|
|
69
69
|
|
|
70
70
|
인터랙티브 설정 (GitHub 접근 방법, 알림 채널, 브랜치 패턴, 에이전트 모델 등).
|
|
71
71
|
|
|
72
|
+
### 사전 점검
|
|
73
|
+
|
|
74
|
+
설정이 올바른지 확인하려면:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx @elyun/bylane preflight
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
또는 Claude Code에서:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
/bylane preflight
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
점검 항목: bylane.json 존재, GitHub CLI 로그인, GITHUB_TOKEN, Slack/Telegram 연동 설정.
|
|
87
|
+
문제가 있으면 항목마다 수정 방법을 안내합니다.
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
── byLane 사전 점검 ──
|
|
91
|
+
|
|
92
|
+
✓ bylane 설정 v1.0
|
|
93
|
+
✓ GitHub CLI (fallback) github.com
|
|
94
|
+
! GitHub Token (fallback) GITHUB_TOKEN 환경변수 없음
|
|
95
|
+
→ export GITHUB_TOKEN=ghp_xxxx
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
모든 에이전트 실행 전 자동으로 점검하여 연동 오류로 인한 중간 실패를 방지합니다.
|
|
99
|
+
|
|
72
100
|
---
|
|
73
101
|
|
|
74
102
|
## 사용법
|
|
@@ -11,7 +11,13 @@ description: byLane 메인 오케스트레이터. 자연어 의도를 파싱해
|
|
|
11
11
|
|
|
12
12
|
## 실행 전 체크
|
|
13
13
|
|
|
14
|
-
1.
|
|
14
|
+
1. 사전 점검 실행:
|
|
15
|
+
```bash
|
|
16
|
+
npx @elyun/bylane preflight
|
|
17
|
+
```
|
|
18
|
+
- 점검 실패 시 안내 메시지를 출력하고 워크플로우를 **중단**한다.
|
|
19
|
+
- `.bylane/bylane.json` 없으면 즉시 `bylane-setup` 스킬 실행.
|
|
20
|
+
|
|
15
21
|
2. `.bylane/state/` 디렉토리 확인. 없으면 생성.
|
|
16
22
|
|
|
17
23
|
## 에이전트별 모델 결정
|
package/commands/bylane.md
CHANGED
|
@@ -25,6 +25,25 @@ description: byLane 메인 커맨드. 자연어로 전체 개발 워크플로우
|
|
|
25
25
|
/bylane status — 현재 상태 한 줄 요약
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
## 사전 점검 (모든 명령 전 자동 실행)
|
|
29
|
+
|
|
30
|
+
서브커맨드를 라우팅하기 전 아래 점검을 실행한다:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx @elyun/bylane preflight
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
점검 항목:
|
|
37
|
+
- `.bylane/bylane.json` 존재 여부 → 없으면 `/bylane setup` 안내 후 중단
|
|
38
|
+
- GitHub 접근 방법 (`github.method` 기준):
|
|
39
|
+
- `cli`: `gh auth status` → 로그인 안 됐으면 `gh auth login` 안내
|
|
40
|
+
- `api`: `GITHUB_TOKEN` 환경변수 → 없으면 설정 방법 안내
|
|
41
|
+
- `auto`/`mcp`: CLI + Token 둘 다 확인, 어느 것도 없으면 안내
|
|
42
|
+
- 알림 채널 (활성화된 경우만): Slack 채널 설정 여부, Telegram 토큰 여부
|
|
43
|
+
|
|
44
|
+
문제가 있으면 각 항목마다 수정 방법을 출력하고 중단한다.
|
|
45
|
+
`setup`, `status`, `preflight` 서브커맨드는 점검 없이 바로 실행한다.
|
|
46
|
+
|
|
28
47
|
## 실행 흐름
|
|
29
48
|
|
|
30
49
|
첫 번째 인자가 서브커맨드인지 확인.
|
|
@@ -47,6 +66,7 @@ description: byLane 메인 커맨드. 자연어로 전체 개발 워크플로우
|
|
|
47
66
|
| `respond-loop` | `bylane-respond-loop` |
|
|
48
67
|
| `notify` | `bylane-notify-agent` |
|
|
49
68
|
| `status` | `.bylane/state/` 파일 읽어 한 줄 요약 출력 |
|
|
69
|
+
| `preflight` | 연동 상태 점검 및 문제 안내 |
|
|
50
70
|
|
|
51
71
|
## monitor 서브커맨드
|
|
52
72
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -141,6 +141,11 @@ function install() {
|
|
|
141
141
|
|
|
142
142
|
if (command === 'install') {
|
|
143
143
|
install()
|
|
144
|
+
} else if (command === 'preflight') {
|
|
145
|
+
const { runPreflight, formatPreflight } = await import('./preflight.js')
|
|
146
|
+
const result = runPreflight()
|
|
147
|
+
console.log(formatPreflight(result))
|
|
148
|
+
if (!result.passed) process.exit(1)
|
|
144
149
|
} else if (command === 'models') {
|
|
145
150
|
// models → 에이전트별 모델 목록 출력 (KEY=VALUE 형식)
|
|
146
151
|
const { loadConfig, getAgentModel } = await import('./config.js')
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* loop-utils.js
|
|
3
|
+
* 루프 프로세스 공통 유틸
|
|
4
|
+
*/
|
|
5
|
+
import { execSync } from 'child_process'
|
|
6
|
+
import { readState, writeState } from './state.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 동일 루프가 이미 실행 중이면 기존 프로세스를 종료하고 기다린다.
|
|
10
|
+
* @param {string} loopName e.g. 'review-loop'
|
|
11
|
+
* @param {string} stateDir
|
|
12
|
+
*/
|
|
13
|
+
export function killExistingLoop(loopName, stateDir = '.bylane/state') {
|
|
14
|
+
const existing = readState(loopName, stateDir)
|
|
15
|
+
if (!existing || existing.status !== 'running' || !existing.pid) return
|
|
16
|
+
|
|
17
|
+
const pid = Number(existing.pid)
|
|
18
|
+
if (pid === process.pid) return // 자기 자신이면 무시
|
|
19
|
+
|
|
20
|
+
// PID가 살아있는지 확인
|
|
21
|
+
try {
|
|
22
|
+
process.kill(pid, 0)
|
|
23
|
+
} catch {
|
|
24
|
+
// 이미 종료된 프로세스 — 상태만 정리
|
|
25
|
+
writeState(loopName, { ...existing, status: 'stopped', stoppedAt: new Date().toISOString() }, stateDir)
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 기존 프로세스 종료
|
|
30
|
+
console.log(`[${loopName}] 기존 프로세스(PID: ${pid}) 종료 후 재시작합니다.`)
|
|
31
|
+
try {
|
|
32
|
+
process.kill(pid, 'SIGTERM')
|
|
33
|
+
} catch {
|
|
34
|
+
// 종료 실패 시 무시하고 계속
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 최대 2초 대기 (100ms 간격 폴링)
|
|
38
|
+
const deadline = Date.now() + 2000
|
|
39
|
+
while (Date.now() < deadline) {
|
|
40
|
+
try {
|
|
41
|
+
process.kill(pid, 0)
|
|
42
|
+
execSync('sleep 0.1')
|
|
43
|
+
} catch {
|
|
44
|
+
break // 종료 완료
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -18,6 +18,21 @@ const STATUS_ICON = {
|
|
|
18
18
|
escalated: '[!]'
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
const STATUS_COLOR = {
|
|
22
|
+
idle: '',
|
|
23
|
+
in_progress: '{yellow-fg}',
|
|
24
|
+
running: '{green-fg}',
|
|
25
|
+
completed: '{green-fg}',
|
|
26
|
+
stopped: '{grey-fg}',
|
|
27
|
+
failed: '{red-fg}',
|
|
28
|
+
escalated: '{red-fg}'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function colorize(text, status) {
|
|
32
|
+
const c = STATUS_COLOR[status] ?? ''
|
|
33
|
+
return c ? `${c}${text}{/}` : text
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
export function createPipelinePanel(screen) {
|
|
22
37
|
const box = blessed.box({
|
|
23
38
|
top: 3,
|
|
@@ -35,7 +50,7 @@ export function createPipelinePanel(screen) {
|
|
|
35
50
|
update(states, subagents = { active: [], recent: [] }) {
|
|
36
51
|
const lines = AGENTS.map(name => {
|
|
37
52
|
const s = states[name]
|
|
38
|
-
if (!s) return ` ${STATUS_ICON.idle} ${name.padEnd(16)}
|
|
53
|
+
if (!s) return ` {grey-fg}${STATUS_ICON.idle} ${name.padEnd(16)} 대기{/}`
|
|
39
54
|
const icon = STATUS_ICON[s.status] ?? STATUS_ICON.idle
|
|
40
55
|
const elapsed = s.startedAt
|
|
41
56
|
? `${Math.floor((Date.now() - new Date(s.startedAt)) / 1000)}s`
|
|
@@ -43,7 +58,7 @@ export function createPipelinePanel(screen) {
|
|
|
43
58
|
const bar = s.progress > 0
|
|
44
59
|
? `${'#'.repeat(Math.floor(s.progress / 10))}${'-'.repeat(10 - Math.floor(s.progress / 10))} ${s.progress}%`
|
|
45
60
|
: ''
|
|
46
|
-
return ` ${icon} ${name.padEnd(16)} ${elapsed.padEnd(6)} ${bar}`
|
|
61
|
+
return ` ${colorize(`${icon} ${name.padEnd(16)}`, s.status)} ${elapsed.padEnd(6)} ${bar}`
|
|
47
62
|
})
|
|
48
63
|
|
|
49
64
|
const retries = states['orchestrator']?.retries ?? 0
|
|
@@ -57,13 +72,13 @@ export function createPipelinePanel(screen) {
|
|
|
57
72
|
for (const name of allLoops) {
|
|
58
73
|
const s = states[name]
|
|
59
74
|
if (!s) {
|
|
60
|
-
lines.push(` [-] ${name.padEnd(16)}
|
|
75
|
+
lines.push(` {grey-fg}[-] ${name.padEnd(16)} 미실행{/}`)
|
|
61
76
|
} else {
|
|
62
77
|
const icon = STATUS_ICON[s.status] ?? '[-]'
|
|
63
78
|
const elapsed = s.startedAt
|
|
64
79
|
? `${Math.floor((Date.now() - new Date(s.startedAt)) / 1000)}s`
|
|
65
80
|
: ''
|
|
66
|
-
lines.push(` ${icon} ${name.padEnd(16)} ${elapsed}`)
|
|
81
|
+
lines.push(` ${colorize(`${icon} ${name.padEnd(16)}`, s.status)} ${elapsed}`)
|
|
67
82
|
}
|
|
68
83
|
}
|
|
69
84
|
|
package/src/preflight.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* preflight.js
|
|
3
|
+
* GitHub CLI/MCP/API, 알림 채널 연동 상태를 점검하고 문제가 있으면 가이드를 출력한다.
|
|
4
|
+
* `npx @elyun/bylane preflight` 또는 에이전트 시작 전 자동 실행.
|
|
5
|
+
*/
|
|
6
|
+
import { execSync } from 'child_process'
|
|
7
|
+
import { existsSync } from 'fs'
|
|
8
|
+
import { loadConfig } from './config.js'
|
|
9
|
+
|
|
10
|
+
const CONFIG_PATH = '.bylane/bylane.json'
|
|
11
|
+
|
|
12
|
+
function run(cmd) {
|
|
13
|
+
try {
|
|
14
|
+
return { ok: true, out: execSync(cmd, { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] }).trim() }
|
|
15
|
+
} catch (e) {
|
|
16
|
+
return { ok: false, out: e.stderr?.toString().trim() ?? '' }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function checkGhCli() {
|
|
21
|
+
const which = run('which gh')
|
|
22
|
+
if (!which.ok) return { ok: false, reason: 'gh CLI 미설치', fix: 'brew install gh # 또는 https://cli.github.com' }
|
|
23
|
+
|
|
24
|
+
const auth = run('gh auth status')
|
|
25
|
+
if (!auth.ok) return { ok: false, reason: 'gh CLI 로그인 필요', fix: 'gh auth login' }
|
|
26
|
+
|
|
27
|
+
return { ok: true, detail: auth.out.split('\n')[0] }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function checkGithubToken() {
|
|
31
|
+
const token = process.env.GITHUB_TOKEN
|
|
32
|
+
if (!token) return { ok: false, reason: 'GITHUB_TOKEN 환경변수 없음', fix: 'export GITHUB_TOKEN=ghp_xxxx' }
|
|
33
|
+
if (token.length < 10) return { ok: false, reason: 'GITHUB_TOKEN 값이 너무 짧음', fix: '올바른 토큰을 설정하세요' }
|
|
34
|
+
return { ok: true, detail: `설정됨 (${token.slice(0,4)}…)` }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function checkSlack(config) {
|
|
38
|
+
if (!config.notifications?.slack?.enabled) return null
|
|
39
|
+
const channel = config.notifications.slack.channel
|
|
40
|
+
if (!channel) return { ok: false, reason: 'slack.channel 미설정', fix: '.bylane/bylane.json의 notifications.slack.channel 설정' }
|
|
41
|
+
return { ok: true, detail: `채널: ${channel}` }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function checkTelegram(config) {
|
|
45
|
+
if (!config.notifications?.telegram?.enabled) return null
|
|
46
|
+
const botToken = process.env.TELEGRAM_BOT_TOKEN
|
|
47
|
+
const chatId = process.env.TELEGRAM_CHAT_ID || config.notifications.telegram.chatId
|
|
48
|
+
if (!botToken) return { ok: false, reason: 'TELEGRAM_BOT_TOKEN 환경변수 없음', fix: 'export TELEGRAM_BOT_TOKEN=xxx' }
|
|
49
|
+
if (!chatId) return { ok: false, reason: 'TELEGRAM_CHAT_ID 미설정', fix: 'export TELEGRAM_CHAT_ID=xxx 또는 bylane.json에 설정' }
|
|
50
|
+
return { ok: true, detail: `chat_id: ${chatId}` }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 전체 점검 실행
|
|
55
|
+
* @returns {{ passed: boolean, results: Array<{name, ok, detail?, reason?, fix?}> }}
|
|
56
|
+
*/
|
|
57
|
+
export function runPreflight() {
|
|
58
|
+
const results = []
|
|
59
|
+
|
|
60
|
+
// 1. bylane.json 존재 여부
|
|
61
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
62
|
+
results.push({
|
|
63
|
+
name: 'bylane 설정',
|
|
64
|
+
ok: false,
|
|
65
|
+
reason: '.bylane/bylane.json 없음',
|
|
66
|
+
fix: '/bylane setup 을 실행하여 초기 설정을 완료하세요.'
|
|
67
|
+
})
|
|
68
|
+
return { passed: false, results }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let config
|
|
72
|
+
try { config = loadConfig() } catch (e) {
|
|
73
|
+
results.push({ name: 'bylane 설정', ok: false, reason: `bylane.json 파싱 오류: ${e.message}`, fix: '/bylane setup 으로 재생성' })
|
|
74
|
+
return { passed: false, results }
|
|
75
|
+
}
|
|
76
|
+
results.push({ name: 'bylane 설정', ok: true, detail: `v${config.version ?? '?'}` })
|
|
77
|
+
|
|
78
|
+
// 2. GitHub 접근
|
|
79
|
+
const method = config.github?.method ?? 'auto'
|
|
80
|
+
|
|
81
|
+
if (method === 'cli') {
|
|
82
|
+
const r = checkGhCli()
|
|
83
|
+
results.push({ name: 'GitHub CLI', ...r })
|
|
84
|
+
} else if (method === 'api') {
|
|
85
|
+
const r = checkGithubToken()
|
|
86
|
+
results.push({ name: 'GitHub Token', ...r })
|
|
87
|
+
} else if (method === 'auto' || method === 'mcp') {
|
|
88
|
+
// auto/mcp: CLI도 확인해두면 유용
|
|
89
|
+
const cli = checkGhCli()
|
|
90
|
+
const token = checkGithubToken()
|
|
91
|
+
const anyOk = cli.ok || token.ok
|
|
92
|
+
|
|
93
|
+
if (method === 'mcp') {
|
|
94
|
+
results.push({
|
|
95
|
+
name: 'GitHub MCP',
|
|
96
|
+
ok: true,
|
|
97
|
+
detail: 'Claude Code 세션에서 자동 사용 (별도 확인 불필요)'
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
results.push({ name: 'GitHub CLI (fallback)', ...cli })
|
|
101
|
+
results.push({ name: 'GitHub Token (fallback)', ...token })
|
|
102
|
+
|
|
103
|
+
if (!anyOk && method === 'auto') {
|
|
104
|
+
results.find(r => r.name === 'GitHub CLI (fallback)').critical = true
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 3. 알림 채널 (설정된 경우만)
|
|
109
|
+
const slack = checkSlack(config)
|
|
110
|
+
if (slack) results.push({ name: 'Slack 알림', ...slack })
|
|
111
|
+
|
|
112
|
+
const telegram = checkTelegram(config)
|
|
113
|
+
if (telegram) results.push({ name: 'Telegram 알림', ...telegram })
|
|
114
|
+
|
|
115
|
+
const passed = results.every(r => r.ok)
|
|
116
|
+
return { passed, results }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 결과를 읽기 좋은 텍스트로 포맷 */
|
|
120
|
+
export function formatPreflight({ passed, results }) {
|
|
121
|
+
const lines = []
|
|
122
|
+
lines.push('')
|
|
123
|
+
lines.push(' ── byLane 사전 점검 ──')
|
|
124
|
+
lines.push('')
|
|
125
|
+
|
|
126
|
+
for (const r of results) {
|
|
127
|
+
const icon = r.ok ? ' ✓' : (r.critical ? ' ✗' : ' !')
|
|
128
|
+
const name = r.name.padEnd(22)
|
|
129
|
+
const desc = r.ok ? (r.detail ?? 'OK') : r.reason
|
|
130
|
+
lines.push(`${icon} ${name} ${desc}`)
|
|
131
|
+
if (!r.ok && r.fix) {
|
|
132
|
+
lines.push(` → ${r.fix}`)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
lines.push('')
|
|
137
|
+
if (passed) {
|
|
138
|
+
lines.push(' 모든 항목 정상. 워크플로우를 시작합니다.')
|
|
139
|
+
} else {
|
|
140
|
+
lines.push(' 일부 항목에 문제가 있습니다. 위 안내를 참고하여 설정을 완료하세요.')
|
|
141
|
+
lines.push(' 설정 완료 후 명령을 다시 실행하거나 /bylane setup 을 실행하세요.')
|
|
142
|
+
}
|
|
143
|
+
lines.push('')
|
|
144
|
+
|
|
145
|
+
return lines.join('\n')
|
|
146
|
+
}
|
package/src/respond-loop.js
CHANGED
|
@@ -8,11 +8,13 @@ import { execSync } from 'child_process'
|
|
|
8
8
|
import { mkdirSync } from 'fs'
|
|
9
9
|
import { writeState, readState, appendLog } from './state.js'
|
|
10
10
|
import { loadConfig } from './config.js'
|
|
11
|
+
import { killExistingLoop } from './loop-utils.js'
|
|
11
12
|
|
|
12
13
|
const INTERVAL_MS = 5 * 60 * 1000
|
|
13
14
|
const STATE_DIR = '.bylane/state'
|
|
14
15
|
|
|
15
16
|
mkdirSync(STATE_DIR, { recursive: true })
|
|
17
|
+
killExistingLoop('respond-loop', STATE_DIR)
|
|
16
18
|
|
|
17
19
|
function fetchMyPRsWithReviews(method, owner, repo) {
|
|
18
20
|
// CLI
|
package/src/review-loop.js
CHANGED
|
@@ -7,11 +7,13 @@ import { execSync } from 'child_process'
|
|
|
7
7
|
import { mkdirSync } from 'fs'
|
|
8
8
|
import { writeState, readState, appendLog } from './state.js'
|
|
9
9
|
import { loadConfig } from './config.js'
|
|
10
|
+
import { killExistingLoop } from './loop-utils.js'
|
|
10
11
|
|
|
11
12
|
const INTERVAL_MS = 5 * 60 * 1000
|
|
12
13
|
const STATE_DIR = '.bylane/state'
|
|
13
14
|
|
|
14
15
|
mkdirSync(STATE_DIR, { recursive: true })
|
|
16
|
+
killExistingLoop('review-loop', STATE_DIR)
|
|
15
17
|
|
|
16
18
|
function fetchPendingReviews(method, owner, repo) {
|
|
17
19
|
// CLI
|