@elyun/bylane 1.17.0 → 1.19.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 +290 -121
- package/commands/bylane-cleanup.md +36 -0
- package/commands/bylane-code-agent.md +3 -24
- package/commands/bylane-commit-agent.md +3 -19
- package/commands/bylane-issue-agent.md +1 -1
- package/commands/bylane-notify-agent.md +1 -1
- package/commands/bylane-orchestrator.md +11 -24
- package/commands/bylane-pr-agent.md +2 -2
- package/commands/bylane-respond-agent.md +1 -1
- package/commands/bylane-respond-loop.md +6 -18
- package/commands/bylane-review-agent.md +2 -2
- package/commands/bylane-review-loop.md +6 -17
- package/commands/bylane-test-agent.md +1 -1
- package/commands/bylane.md +20 -0
- package/package.json +1 -1
- package/src/cleanup.js +152 -0
- package/src/cli.js +44 -0
- package/src/loop-utils.js +47 -0
- package/src/monitor/index.js +29 -0
- package/src/monitor/layout.js +1 -1
- package/src/preflight.js +146 -0
- package/src/respond-loop.js +2 -0
- package/src/review-loop.js +2 -0
|
@@ -14,7 +14,7 @@ description: 워크플로우 완료 또는 개입 필요 시 Slack/Telegram으
|
|
|
14
14
|
## 실행 전 상태 기록
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
|
|
17
|
+
npx @elyun/bylane state write notify-agent '{"status":"in_progress","startedAt":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","progress":0,"retries":0,"log":[]}'
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
## 실행 흐름
|
|
@@ -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
|
## 에이전트별 모델 결정
|
|
@@ -19,19 +25,11 @@ description: byLane 메인 오케스트레이터. 자연어 의도를 파싱해
|
|
|
19
25
|
각 에이전트 실행 전 사용할 모델을 config에서 읽는다:
|
|
20
26
|
|
|
21
27
|
```bash
|
|
22
|
-
|
|
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
|
-
// analyze-agent는 항상 opus 사용 (config 무관)
|
|
31
|
-
})
|
|
32
|
-
"
|
|
28
|
+
npx @elyun/bylane models
|
|
33
29
|
```
|
|
34
30
|
|
|
31
|
+
출력 형식: `AGENT_NAME=MODEL_ID` (한 줄씩)
|
|
32
|
+
|
|
35
33
|
에이전트 호출 시 해당 모델을 `model` 파라미터로 전달한다.
|
|
36
34
|
|
|
37
35
|
## 의도 파싱 규칙
|
|
@@ -57,18 +55,7 @@ import('./src/config.js').then(({loadConfig, getAgentModel}) => {
|
|
|
57
55
|
|
|
58
56
|
상태 기록 (각 에이전트 시작 전):
|
|
59
57
|
```bash
|
|
60
|
-
|
|
61
|
-
import('./src/state.js').then(({writeState}) => {
|
|
62
|
-
writeState('AGENT_NAME', {
|
|
63
|
-
status: 'in_progress',
|
|
64
|
-
startedAt: new Date().toISOString(),
|
|
65
|
-
progress: 0,
|
|
66
|
-
currentTask: 'TASK_DESCRIPTION',
|
|
67
|
-
retries: 0,
|
|
68
|
-
log: []
|
|
69
|
-
})
|
|
70
|
-
})
|
|
71
|
-
"
|
|
58
|
+
npx @elyun/bylane state write AGENT_NAME '{"status":"in_progress","startedAt":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","progress":0,"currentTask":"TASK_DESCRIPTION","retries":0,"log":[]}'
|
|
72
59
|
```
|
|
73
60
|
|
|
74
61
|
## 피드백 루프
|
|
@@ -43,7 +43,7 @@ ls .github/PULL_REQUEST_TEMPLATE/*.md 2>/dev/null | head -5
|
|
|
43
43
|
## 실행 전 상태 기록
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
|
|
46
|
+
npx @elyun/bylane state write pr-agent '{"status":"in_progress","startedAt":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","progress":0,"retries":0,"log":[]}'
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
## 실행 흐름
|
|
@@ -90,7 +90,7 @@ node -e "import('./src/state.js').then(({writeState})=>writeState('pr-agent',{st
|
|
|
90
90
|
|
|
91
91
|
4. 상태 업데이트:
|
|
92
92
|
```bash
|
|
93
|
-
|
|
93
|
+
npx @elyun/bylane state write pr-agent '{"status":"completed","progress":100,"prNumber":PR_NUMBER,"prUrl":"PR_URL"}'
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
## 출력
|
|
@@ -64,7 +64,7 @@ ls .github/REVIEW_RESPONSE_TEMPLATE.md \
|
|
|
64
64
|
## 실행 전 상태 기록
|
|
65
65
|
|
|
66
66
|
```bash
|
|
67
|
-
|
|
67
|
+
npx @elyun/bylane state write respond-agent '{"status":"in_progress","startedAt":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","progress":0,"retries":0,"log":[]}'
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
## 실행 흐름
|
|
@@ -30,15 +30,12 @@ echo "폴러 PID: $!"
|
|
|
30
30
|
pending PR이 생길 때마다 respond-agent를 실행한다:
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
"
|
|
33
|
+
# 큐 확인 (pending 항목 필터링)
|
|
34
|
+
npx @elyun/bylane state read respond-queue
|
|
40
35
|
```
|
|
41
36
|
|
|
37
|
+
출력된 JSON에서 `queue` 배열의 `status === "pending"` 항목을 선택한다.
|
|
38
|
+
|
|
42
39
|
pending 항목이 있으면 각 PR에 대해:
|
|
43
40
|
|
|
44
41
|
1. `hasChangesRequested` 여부 확인:
|
|
@@ -50,17 +47,8 @@ pending 항목이 있으면 각 PR에 대해:
|
|
|
50
47
|
3. 완료 후 큐 항목을 `status: "responded"`로 업데이트:
|
|
51
48
|
|
|
52
49
|
```bash
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
"
|
|
50
|
+
# 현재 큐 읽기 후 PR_NUMBER 항목을 responded로 업데이트하여 다시 쓰기
|
|
51
|
+
npx @elyun/bylane state write respond-queue '{"status":"running","queue":UPDATED_QUEUE_JSON}'
|
|
64
52
|
```
|
|
65
53
|
|
|
66
54
|
4. 다음 pending 항목으로 반복. pending 없으면 5분 대기 후 재확인 (폴러 주기와 동일).
|
|
@@ -69,7 +69,7 @@ Enter 또는 아무것도 선택하지 않으면 → `all` (전체 검사)
|
|
|
69
69
|
## 실행 전 상태 기록
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
|
-
|
|
72
|
+
npx @elyun/bylane state write review-agent '{"status":"in_progress","startedAt":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","progress":0,"retries":0,"log":[]}'
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
## 실행 흐름
|
|
@@ -180,7 +180,7 @@ curl -s -X POST \
|
|
|
180
180
|
### 5. 상태 업데이트
|
|
181
181
|
|
|
182
182
|
```bash
|
|
183
|
-
|
|
183
|
+
npx @elyun/bylane state write review-agent '{"status":"completed","progress":100,"approved":APPROVED_BOOL,"commentCount":COMMENT_COUNT}'
|
|
184
184
|
```
|
|
185
185
|
|
|
186
186
|
## 출력
|
|
@@ -47,30 +47,19 @@ node src/review-loop.js
|
|
|
47
47
|
아래 루프를 실행하면서 pending PR이 생길 때마다 review-agent를 실행한다:
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
|
-
# 큐 확인
|
|
51
|
-
|
|
52
|
-
import('./src/state.js').then(({readState}) => {
|
|
53
|
-
const q = readState('review-queue', '.bylane/state')
|
|
54
|
-
const pending = (q?.queue ?? []).filter(p => p.status === 'pending')
|
|
55
|
-
console.log(JSON.stringify(pending))
|
|
56
|
-
})
|
|
57
|
-
"
|
|
50
|
+
# 큐 확인 (pending 항목 필터링)
|
|
51
|
+
npx @elyun/bylane state read review-queue
|
|
58
52
|
```
|
|
59
53
|
|
|
54
|
+
출력된 JSON에서 `queue` 배열의 `status === "pending"` 항목을 선택한다.
|
|
55
|
+
|
|
60
56
|
pending 항목이 있으면 각 PR에 대해:
|
|
61
57
|
1. `bylane-review-agent` skill 실행 (PR 번호 전달)
|
|
62
58
|
2. 리뷰 완료 후 큐 항목을 `status: "reviewed"`로 업데이트:
|
|
63
59
|
|
|
64
60
|
```bash
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const q = readState('review-queue', '.bylane/state')
|
|
68
|
-
const queue = (q?.queue ?? []).map(p =>
|
|
69
|
-
p.number === PR_NUMBER ? { ...p, status: 'reviewed', reviewedAt: new Date().toISOString() } : p
|
|
70
|
-
)
|
|
71
|
-
writeState('review-queue', { status: 'running', queue }, '.bylane/state')
|
|
72
|
-
})
|
|
73
|
-
"
|
|
61
|
+
# 현재 큐 읽기 후 PR_NUMBER 항목을 reviewed로 업데이트하여 다시 쓰기
|
|
62
|
+
npx @elyun/bylane state write review-queue '{"status":"running","queue":UPDATED_QUEUE_JSON}'
|
|
74
63
|
```
|
|
75
64
|
|
|
76
65
|
3. 다음 pending 항목으로 반복
|
|
@@ -12,7 +12,7 @@ description: 변경된 코드의 테스트를 실행하고 결과를 반환한
|
|
|
12
12
|
## 실행 전 상태 기록
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
|
|
15
|
+
npx @elyun/bylane state write test-agent '{"status":"in_progress","startedAt":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","progress":0,"retries":0,"log":[]}'
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
## 실행 흐름
|
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/cleanup.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cleanup.js
|
|
3
|
+
* 상태 파일 정리, 권한 수정, 좀비 프로세스/에이전트 초기화
|
|
4
|
+
* monitor [r] 키 또는 `npx @elyun/bylane cleanup`으로 실행
|
|
5
|
+
*/
|
|
6
|
+
import { readdirSync, chmodSync, existsSync, unlinkSync, readFileSync, writeFileSync, mkdirSync } from 'fs'
|
|
7
|
+
import { join } from 'path'
|
|
8
|
+
import { readState, writeState } from './state.js'
|
|
9
|
+
|
|
10
|
+
const STATE_DIR = '.bylane/state'
|
|
11
|
+
const STALE_MS = 30 * 60 * 1000 // 30분 이상 in_progress → 초기화
|
|
12
|
+
const SUBAGENTS_FILE = join(STATE_DIR, 'subagents.json')
|
|
13
|
+
|
|
14
|
+
/** 프로세스가 살아있는지 확인 (kill -0) */
|
|
15
|
+
function isPidAlive(pid) {
|
|
16
|
+
try {
|
|
17
|
+
process.kill(Number(pid), 0)
|
|
18
|
+
return true
|
|
19
|
+
} catch {
|
|
20
|
+
return false
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** subagents.json에서 죽은 PID 제거 */
|
|
25
|
+
function cleanSubagents() {
|
|
26
|
+
if (!existsSync(SUBAGENTS_FILE)) return []
|
|
27
|
+
let data
|
|
28
|
+
try { data = JSON.parse(readFileSync(SUBAGENTS_FILE, 'utf8')) } catch { return [] }
|
|
29
|
+
|
|
30
|
+
const before = (data.active ?? []).length
|
|
31
|
+
const active = (data.active ?? []).filter(a => {
|
|
32
|
+
if (!a.pid) return false
|
|
33
|
+
return isPidAlive(a.pid)
|
|
34
|
+
})
|
|
35
|
+
const cleaned = before - active.length
|
|
36
|
+
|
|
37
|
+
if (cleaned > 0) {
|
|
38
|
+
writeFileSync(SUBAGENTS_FILE, JSON.stringify({ ...data, active }, null, 2))
|
|
39
|
+
}
|
|
40
|
+
return cleaned > 0 ? [`subagents.json: active ${before}개 → ${active.length}개 (${cleaned}개 제거)`] : []
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 디렉토리 및 파일 권한 수정 */
|
|
44
|
+
function fixPermissions() {
|
|
45
|
+
const fixed = []
|
|
46
|
+
try {
|
|
47
|
+
chmodSync(STATE_DIR, 0o755)
|
|
48
|
+
} catch {}
|
|
49
|
+
|
|
50
|
+
const files = existsSync(STATE_DIR)
|
|
51
|
+
? readdirSync(STATE_DIR).filter(f => f.endsWith('.json'))
|
|
52
|
+
: []
|
|
53
|
+
|
|
54
|
+
for (const file of files) {
|
|
55
|
+
try {
|
|
56
|
+
chmodSync(join(STATE_DIR, file), 0o644)
|
|
57
|
+
} catch (e) {
|
|
58
|
+
fixed.push(`권한 수정 실패: ${file} — ${e.message}`)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return fixed
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 전체 정리 실행
|
|
66
|
+
* @returns {{ fixed: string[], killed: string[], reset: string[], cleared: string[] }}
|
|
67
|
+
*/
|
|
68
|
+
export function runCleanup() {
|
|
69
|
+
const result = { fixed: [], killed: [], reset: [], cleared: [] }
|
|
70
|
+
|
|
71
|
+
if (!existsSync(STATE_DIR)) {
|
|
72
|
+
mkdirSync(STATE_DIR, { recursive: true })
|
|
73
|
+
return result
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 1. 파일 권한 수정
|
|
77
|
+
result.fixed.push(...fixPermissions())
|
|
78
|
+
|
|
79
|
+
// 2. 상태 파일 순회
|
|
80
|
+
const files = readdirSync(STATE_DIR).filter(f => f.endsWith('.json'))
|
|
81
|
+
|
|
82
|
+
for (const file of files) {
|
|
83
|
+
const name = file.replace('.json', '')
|
|
84
|
+
|
|
85
|
+
// 특수 파일 건너뜀
|
|
86
|
+
if (name === 'cancel') continue
|
|
87
|
+
|
|
88
|
+
// subagents 별도 처리
|
|
89
|
+
if (name === 'subagents') {
|
|
90
|
+
result.cleared.push(...cleanSubagents())
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const state = readState(name, STATE_DIR)
|
|
95
|
+
if (!state) continue
|
|
96
|
+
|
|
97
|
+
// 루프: PID가 죽었으면 stopped로 전환
|
|
98
|
+
if (name.endsWith('-loop') && state.pid && state.status === 'running') {
|
|
99
|
+
if (!isPidAlive(state.pid)) {
|
|
100
|
+
writeState(name, { ...state, status: 'stopped', stoppedAt: new Date().toISOString() }, STATE_DIR)
|
|
101
|
+
result.killed.push(`${name}: PID ${state.pid} 없음 → stopped`)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// in_progress 상태가 30분 이상 → failed로 초기화
|
|
106
|
+
if (state.status === 'in_progress' && state.startedAt) {
|
|
107
|
+
const age = Date.now() - new Date(state.startedAt).getTime()
|
|
108
|
+
if (age > STALE_MS) {
|
|
109
|
+
writeState(name, {
|
|
110
|
+
...state,
|
|
111
|
+
status: 'failed',
|
|
112
|
+
error: `stale: ${Math.floor(age / 60000)}분 초과 in_progress`
|
|
113
|
+
}, STATE_DIR)
|
|
114
|
+
result.reset.push(`${name}: ${Math.floor(age / 60000)}분 in_progress → failed`)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 큐 파일: responding/reviewing 상태가 남아있으면 pending으로 복구
|
|
119
|
+
if ((name === 'review-queue' || name === 'respond-queue') && Array.isArray(state.queue)) {
|
|
120
|
+
const fixed = state.queue.map(item =>
|
|
121
|
+
item.status === 'reviewing' || item.status === 'responding'
|
|
122
|
+
? { ...item, status: 'pending', recoveredAt: new Date().toISOString() }
|
|
123
|
+
: item
|
|
124
|
+
)
|
|
125
|
+
const changedCount = fixed.filter((item, i) => item.status !== state.queue[i].status).length
|
|
126
|
+
if (changedCount > 0) {
|
|
127
|
+
writeState(name, { ...state, queue: fixed }, STATE_DIR)
|
|
128
|
+
result.reset.push(`${name}: ${changedCount}개 진행중 항목 → pending 복구`)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return result
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 결과를 사람이 읽기 쉬운 문자열로 출력 */
|
|
137
|
+
export function formatCleanupResult(result) {
|
|
138
|
+
const lines = []
|
|
139
|
+
const total = Object.values(result).reduce((s, arr) => s + arr.length, 0)
|
|
140
|
+
|
|
141
|
+
if (total === 0) {
|
|
142
|
+
lines.push('정리할 항목 없음.')
|
|
143
|
+
return lines.join('\n')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (result.killed.length) lines.push(' [종료]', ...result.killed.map(s => ` · ${s}`))
|
|
147
|
+
if (result.reset.length) lines.push(' [초기화]', ...result.reset.map(s => ` · ${s}`))
|
|
148
|
+
if (result.cleared.length) lines.push(' [정리]', ...result.cleared.map(s => ` · ${s}`))
|
|
149
|
+
if (result.fixed.length) lines.push(' [오류]', ...result.fixed.map(s => ` · ${s}`))
|
|
150
|
+
|
|
151
|
+
return lines.join('\n')
|
|
152
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -141,6 +141,50 @@ 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)
|
|
149
|
+
} else if (command === 'models') {
|
|
150
|
+
// models → 에이전트별 모델 목록 출력 (KEY=VALUE 형식)
|
|
151
|
+
const { loadConfig, getAgentModel } = await import('./config.js')
|
|
152
|
+
const config = loadConfig()
|
|
153
|
+
const agents = ['orchestrator','issue-agent','code-agent','test-agent',
|
|
154
|
+
'commit-agent','pr-agent','review-agent','respond-agent','notify-agent','analyze-agent']
|
|
155
|
+
agents.forEach(a => console.log(`${a}=${getAgentModel(config, a)}`))
|
|
156
|
+
} else if (command === 'branch') {
|
|
157
|
+
// branch ISSUE_NUMBER → 브랜치명 출력
|
|
158
|
+
const issueNumber = Number(args[1])
|
|
159
|
+
if (!issueNumber) { console.error('사용법: bylane branch <issueNumber>'); process.exit(1) }
|
|
160
|
+
const { buildBranchNameFromConfig } = await import('./branch.js')
|
|
161
|
+
const { loadConfig } = await import('./config.js')
|
|
162
|
+
console.log(buildBranchNameFromConfig(loadConfig(), issueNumber))
|
|
163
|
+
} else if (command === 'state') {
|
|
164
|
+
// state write AGENT '{"status":"in_progress",...}'
|
|
165
|
+
// state append AGENT "메시지"
|
|
166
|
+
// state read AGENT
|
|
167
|
+
const subCmd = args[1]
|
|
168
|
+
const agentName = args[2]
|
|
169
|
+
const payload = args[3]
|
|
170
|
+
const { writeState, appendLog, readState } = await import('./state.js')
|
|
171
|
+
|
|
172
|
+
if (subCmd === 'write' && agentName && payload) {
|
|
173
|
+
writeState(agentName, JSON.parse(payload))
|
|
174
|
+
} else if (subCmd === 'append' && agentName && payload) {
|
|
175
|
+
appendLog(agentName, payload)
|
|
176
|
+
} else if (subCmd === 'read' && agentName) {
|
|
177
|
+
console.log(JSON.stringify(readState(agentName), null, 2))
|
|
178
|
+
} else {
|
|
179
|
+
console.error('사용법: bylane state <write|append|read> <agentName> [payload]')
|
|
180
|
+
process.exit(1)
|
|
181
|
+
}
|
|
182
|
+
} else if (command === 'cleanup') {
|
|
183
|
+
const { runCleanup, formatCleanupResult } = await import('./cleanup.js')
|
|
184
|
+
console.log('\n byLane 상태 정리 중...\n')
|
|
185
|
+
const result = runCleanup()
|
|
186
|
+
console.log(formatCleanupResult(result))
|
|
187
|
+
console.log('\n 완료.\n')
|
|
144
188
|
} else if (command === 'monitor') {
|
|
145
189
|
// 항상 현재 패키지의 모니터 실행 (버전 일치 보장)
|
|
146
190
|
const monitorPath = join(__dirname, 'monitor', 'index.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
|
+
}
|
package/src/monitor/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createLayout } from './layout.js'
|
|
|
4
4
|
import { createPoller } from './poller.js'
|
|
5
5
|
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'fs'
|
|
6
6
|
import { join } from 'path'
|
|
7
|
+
import { runCleanup, formatCleanupResult } from '../cleanup.js'
|
|
7
8
|
|
|
8
9
|
const { screen, header, pipeline, log, queue, status, onCleanup } = createLayout()
|
|
9
10
|
const poller = createPoller()
|
|
@@ -101,6 +102,34 @@ screen.key('s', () => {
|
|
|
101
102
|
})
|
|
102
103
|
})
|
|
103
104
|
|
|
105
|
+
// 'r' — 상태 정리 (권한 수정, 좀비 초기화, 큐 복구)
|
|
106
|
+
screen.key('r', () => {
|
|
107
|
+
header.update({
|
|
108
|
+
workflowTitle: '상태 정리 중...',
|
|
109
|
+
time: new Date().toLocaleTimeString('ko-KR', { hour12: false }),
|
|
110
|
+
elapsed: `${Math.floor((Date.now() - startTime) / 1000)}s`
|
|
111
|
+
})
|
|
112
|
+
screen.render()
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const result = runCleanup()
|
|
116
|
+
const summary = formatCleanupResult(result)
|
|
117
|
+
const total = Object.values(result).reduce((s, a) => s + a.length, 0)
|
|
118
|
+
header.update({
|
|
119
|
+
workflowTitle: total > 0 ? `정리 완료 (${total}건)` : '정리 완료 — 이상 없음',
|
|
120
|
+
time: new Date().toLocaleTimeString('ko-KR', { hour12: false }),
|
|
121
|
+
elapsed: `${Math.floor((Date.now() - startTime) / 1000)}s`
|
|
122
|
+
})
|
|
123
|
+
} catch (e) {
|
|
124
|
+
header.update({
|
|
125
|
+
workflowTitle: `정리 실패: ${e.message}`,
|
|
126
|
+
time: new Date().toLocaleTimeString('ko-KR', { hour12: false }),
|
|
127
|
+
elapsed: `${Math.floor((Date.now() - startTime) / 1000)}s`
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
screen.render()
|
|
131
|
+
})
|
|
132
|
+
|
|
104
133
|
// 'c' — 하위 에이전트 취소 플래그 토글
|
|
105
134
|
screen.key('c', () => {
|
|
106
135
|
if (existsSync(CANCEL_FILE)) {
|
package/src/monitor/layout.js
CHANGED
|
@@ -62,7 +62,7 @@ export function createLayout() {
|
|
|
62
62
|
left: 0,
|
|
63
63
|
width: '100%',
|
|
64
64
|
height: 1,
|
|
65
|
-
content: ' [q]종료 [c]에이전트취소토글 [s]루프종료 [Tab]포커스 [j/k]로그스크롤',
|
|
65
|
+
content: ' [q]종료 [r]상태정리 [c]에이전트취소토글 [s]루프종료 [Tab]포커스 [j/k]로그스크롤',
|
|
66
66
|
style: { fg: 'black', bg: 'cyan' }
|
|
67
67
|
})
|
|
68
68
|
screen.append(footer)
|