@elyun/bylane 1.21.0 → 1.23.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/CLAUDE.md +37 -5
- package/README.md +101 -0
- package/commands/bylane-code-agent.md +79 -15
- package/commands/bylane-issue-agent.md +195 -49
- package/commands/bylane-orchestrator.md +29 -26
- package/commands/bylane-setup.md +35 -4
- package/package.json +1 -1
- package/src/cli.js +102 -1
- package/src/config.js +9 -0
- package/src/loop-utils.js +102 -0
- package/src/memory.js +93 -0
- package/src/monitor/index.js +8 -0
- package/src/preflight.js +14 -2
- package/src/respond-loop.js +10 -12
- package/src/review-loop.js +10 -12
package/CLAUDE.md
CHANGED
|
@@ -21,6 +21,15 @@ node -e "import('./src/state.js').then(({writeState})=>writeState('code-agent',{
|
|
|
21
21
|
|
|
22
22
|
# 에이전트별 모델 확인
|
|
23
23
|
node -e "import('./src/config.js').then(({loadConfig,getAgentModel})=>{const c=loadConfig();['orchestrator','code-agent','review-agent'].forEach(a=>console.log(a,getAgentModel(c,a)))})"
|
|
24
|
+
|
|
25
|
+
# Loop 관리
|
|
26
|
+
npx @elyun/bylane loop start # review-loop + respond-loop 시작 (tmux 또는 process)
|
|
27
|
+
npx @elyun/bylane loop stop # loop 종료
|
|
28
|
+
npx @elyun/bylane loop status # 실행 상태 확인
|
|
29
|
+
|
|
30
|
+
# issueMemory 확인
|
|
31
|
+
npx @elyun/bylane memory list
|
|
32
|
+
npx @elyun/bylane memory read 123
|
|
24
33
|
```
|
|
25
34
|
|
|
26
35
|
## 아키텍처
|
|
@@ -28,9 +37,11 @@ node -e "import('./src/config.js').then(({loadConfig,getAgentModel})=>{const c=l
|
|
|
28
37
|
- `src/state.js` — `.bylane/state/*.json` 읽기/쓰기 유틸 (writeState, readState, clearState, listStates, appendLog)
|
|
29
38
|
- `src/config.js` — `.bylane/bylane.json` 로드/저장/검증 (loadConfig, saveConfig, validateConfig, getAgentModel, DEFAULT_CONFIG)
|
|
30
39
|
- `src/branch.js` — 브랜치명 패턴 엔진 (buildBranchName, buildBranchNameFromConfig)
|
|
31
|
-
- `src/
|
|
32
|
-
- `src/
|
|
33
|
-
- `src/
|
|
40
|
+
- `src/memory.js` — 이슈별 컨텍스트 메모리 유틸 (readIssueMemory, appendIssueMemory, listIssueMemories)
|
|
41
|
+
- `src/cli.js` — npx 설치 CLI (install, loop, --symlink 옵션, 기존 파일 .bak 백업)
|
|
42
|
+
- `src/loop-utils.js` — 루프 공통 유틸 (killExistingLoop, tmux 세션 관리, createAbsoluteTimer, resolveLoopMode)
|
|
43
|
+
- `src/review-loop.js` — review 요청 PR 폴러 → `.bylane/state/review-queue.json` (절대시간 기반 폴링)
|
|
44
|
+
- `src/respond-loop.js` — 내 PR 리뷰/코멘트 폴러 → `.bylane/state/respond-queue.json` (절대시간 기반 폴링)
|
|
34
45
|
- `src/monitor/` — blessed 기반 TUI 대시보드 (2열 그리드, 1초 폴링, fullUnicode)
|
|
35
46
|
- `skills/` — Claude Code 에이전트 skill 파일들
|
|
36
47
|
- `hooks/` — 외부 이벤트 자동 감지 훅
|
|
@@ -46,12 +57,31 @@ orchestrator → issue-agent → code-agent → test-agent → commit-agent
|
|
|
46
57
|
→ pr-agent → review-agent → respond-agent → notify-agent
|
|
47
58
|
|
|
48
59
|
analyze-agent (독립: 프로젝트 분석 → .claude/instructions/ 생성)
|
|
49
|
-
review-loop (독립:
|
|
50
|
-
respond-loop (독립:
|
|
60
|
+
review-loop (독립: 설정 주기로 review 요청 감지, 절대시간 기반 폴링)
|
|
61
|
+
respond-loop (독립: 설정 주기로 리뷰 코멘트 감지, 절대시간 기반 폴링)
|
|
51
62
|
```
|
|
52
63
|
|
|
53
64
|
각 에이전트는 `.bylane/state/{name}.json`에 상태 기록. 모니터가 1초마다 폴링.
|
|
54
65
|
|
|
66
|
+
## Loop 실행
|
|
67
|
+
|
|
68
|
+
`bylane loop start` / `bylane loop stop` / `bylane loop status`
|
|
69
|
+
|
|
70
|
+
두 가지 모드 (`config.loop.mode`):
|
|
71
|
+
|
|
72
|
+
| 모드 | 설명 | 해결하는 문제 |
|
|
73
|
+
|------|------|-------------|
|
|
74
|
+
| `tmux` (기본) | tmux 세션에서 백그라운드 실행 | 터미널 종료, SSH 끊김 시 프로세스 유지 |
|
|
75
|
+
| `process` | 현재 프로세스에서 직접 실행 | tmux 미설치 환경 대응 |
|
|
76
|
+
|
|
77
|
+
두 모드 모두 **절대시간 기반 폴링** 사용:
|
|
78
|
+
- 10초마다 "마지막 폴링 후 intervalMs 경과 여부" 체크
|
|
79
|
+
- macOS 잠자기 모드 동안은 CPU 정지로 실행 불가 (OS 제약)
|
|
80
|
+
- 잠자기 해제 직후 경과 시간 감지 → 즉시 폴링 실행
|
|
81
|
+
- preflight에서 tmux 미설치 감지 시 자동으로 process 모드 fallback
|
|
82
|
+
|
|
83
|
+
설정: `config.loop.intervalMs` (기본 300000 = 5분), `config.loop.sessionName` (기본 `bylane-loops`)
|
|
84
|
+
|
|
55
85
|
## 상태 파일 스키마
|
|
56
86
|
|
|
57
87
|
```json
|
|
@@ -92,6 +122,8 @@ tests/
|
|
|
92
122
|
## 주의사항
|
|
93
123
|
|
|
94
124
|
- `.bylane/state/`는 .gitignore로 제외됨 (런타임 상태)
|
|
125
|
+
- `.bylane/memory/`는 .gitignore로 제외됨 (issueMemory 로컬 파일)
|
|
95
126
|
- `.bylane/bylane.json`은 추적됨 (프로젝트 설정)
|
|
96
127
|
- `docs/`는 .gitignore로 제외됨 (내부 설계 문서)
|
|
97
128
|
- pre-commit 훅: `npm install` 시 자동 등록 (`prepare` 스크립트)
|
|
129
|
+
- issueMemory: 루프 비활성 시 로컬 파일, 루프 실행 중 GitHub 이슈 코멘트로 기록
|
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ GitHub Issues에서 시작해 코드 구현, 테스트, 커밋, PR 생성, 리
|
|
|
11
11
|
| 기능 | 설명 |
|
|
12
12
|
|------|------|
|
|
13
13
|
| **전체 워크플로우** | 자연어 한 줄로 이슈 → 코드 → 테스트 → 커밋 → PR 자동화 |
|
|
14
|
+
| **issueMemory** | 이슈별 작업 컨텍스트(아키텍처 결정, 트러블슈팅 등) 자동 기록 및 재활용 |
|
|
14
15
|
| **프로젝트 분석** | 코드 스타일·디자인 토큰·아키텍처 자동 분석, `.claude/instructions/` 생성 |
|
|
15
16
|
| **인라인 코드 리뷰** | 변경 라인마다 개별 코멘트 + GitHub suggestion 블록 자동 등록 |
|
|
16
17
|
| **자동 리뷰 루프** | 5분 주기로 review 요청된 PR을 자동 감지 → 리뷰 실행 |
|
|
@@ -110,6 +111,60 @@ npx @elyun/bylane preflight
|
|
|
110
111
|
/bylane 리뷰 #45 반영해줘
|
|
111
112
|
```
|
|
112
113
|
|
|
114
|
+
### 효과적인 지시 방법
|
|
115
|
+
|
|
116
|
+
byLane은 지시 내용을 바탕으로 코드베이스를 분석하고 사용자와 문답을 거쳐 이슈를 작성합니다.
|
|
117
|
+
지시가 구체적일수록 문답 횟수가 줄고 이슈 품질이 높아집니다.
|
|
118
|
+
|
|
119
|
+
#### 신규 기능
|
|
120
|
+
|
|
121
|
+
목적과 UI 힌트를 함께 제공하면 좋습니다:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
/bylane 헤더 우측에 다크모드 토글 버튼 추가해줘. 상태는 localStorage에 저장하고 시스템 설정도 따라가게 해줘.
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Figma 링크가 있으면 포함하세요:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
/bylane 결제 흐름 추가해줘. 디자인은 https://figma.com/... 참고해줘.
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### 버그 수정
|
|
134
|
+
|
|
135
|
+
재현 조건과 기대 동작을 함께 적으면 분석 범위가 좁아집니다:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
/bylane 로그인 후 새로고침하면 세션이 풀리는 버그 수정해줘. 토큰 갱신 로직 쪽 문제인 것 같아.
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
#### 기존 기능 개선
|
|
142
|
+
|
|
143
|
+
현재 상태의 어느 부분이 문제인지 지적하면 최소 변경으로 진행됩니다:
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
/bylane 상품 목록 API 호출이 매번 발생해서 느린데, 캐싱 넣어줘. SWR 이미 쓰고 있어.
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
#### 이슈 번호로 구현
|
|
150
|
+
|
|
151
|
+
이미 작성된 이슈를 그대로 구현할 때:
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
/bylane issue #42 구현해줘
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
issue-agent가 이슈 본문의 "구현 방향"과 "관련 파일" 섹션을 읽어 code-agent에 전달합니다.
|
|
158
|
+
이슈 본문에 배경, 접근 방법, 영향 범위가 잘 정리되어 있을수록 추가 문답 없이 바로 구현합니다.
|
|
159
|
+
|
|
160
|
+
#### 피해야 할 패턴
|
|
161
|
+
|
|
162
|
+
| 지시 | 문제 | 개선 |
|
|
163
|
+
|------|------|------|
|
|
164
|
+
| `/bylane 버그 고쳐줘` | 어떤 버그인지 불명확 → 문답 증가 | `/bylane 장바구니 수량 변경 시 합계가 안 바뀌는 버그 수정해줘` |
|
|
165
|
+
| `/bylane 리팩토링해줘` | 범위 불명확 → 전체 분석 필요 | `/bylane useCart hook이 너무 커서 상태 관리 부분만 분리해줘` |
|
|
166
|
+
| `/bylane 성능 개선` | 측정 기준 없음 | `/bylane 홈 화면 LCP가 4초 넘는데 이미지 lazy loading 추가해줘` |
|
|
167
|
+
|
|
113
168
|
### 개별 에이전트 실행
|
|
114
169
|
|
|
115
170
|
```
|
|
@@ -266,6 +321,48 @@ auto 모드에서는 실행 전 요약을 먼저 보여주고 확인을 받습
|
|
|
266
321
|
|
|
267
322
|
---
|
|
268
323
|
|
|
324
|
+
## issueMemory
|
|
325
|
+
|
|
326
|
+
이슈 단위로 작업 컨텍스트를 자동으로 기록하고, 다음 세션에서 재활용합니다.
|
|
327
|
+
|
|
328
|
+
### 동작 방식
|
|
329
|
+
|
|
330
|
+
- **루프 비활성 시** — `.bylane/memory/issues/{이슈번호}.md` 로컬 파일에 기록
|
|
331
|
+
- **루프 실행 중** — GitHub 이슈 코멘트로 기록 (팀 전체가 볼 수 있도록)
|
|
332
|
+
|
|
333
|
+
기록 내용: 작업 요약, 변경 파일, 아키텍처 결정, 트러블슈팅 내역
|
|
334
|
+
|
|
335
|
+
### 자동 기록
|
|
336
|
+
|
|
337
|
+
issue-agent와 code-agent가 작업 완료 후 자동으로 기록합니다. 별도 설정 불필요.
|
|
338
|
+
|
|
339
|
+
code-agent는 작업 시작 시 이전 세션의 메모리를 불러와 일관된 아키텍처 결정을 유지합니다.
|
|
340
|
+
|
|
341
|
+
### 수동 사용
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
npx @elyun/bylane memory read 123 # 이슈 #123 메모리 조회
|
|
345
|
+
npx @elyun/bylane memory list # 메모리가 있는 이슈 목록
|
|
346
|
+
npx @elyun/bylane memory append 123 code-agent "메모 내용" # 직접 추가
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### 설정
|
|
350
|
+
|
|
351
|
+
`.bylane/bylane.json`:
|
|
352
|
+
|
|
353
|
+
```json
|
|
354
|
+
{
|
|
355
|
+
"memory": {
|
|
356
|
+
"enabled": true,
|
|
357
|
+
"dir": ".bylane/memory"
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
`enabled: false`로 비활성화할 수 있습니다.
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
269
366
|
## 프로젝트 분석
|
|
270
367
|
|
|
271
368
|
`/bylane analyze` 실행 시 현재 프로젝트를 자동 분석하여 Claude Code가 참조할 instruction 파일을 생성합니다.
|
|
@@ -355,6 +452,10 @@ respond-loop 독립: 5분 주기 리뷰 코멘트 감지
|
|
|
355
452
|
"templateFile": "",
|
|
356
453
|
"footer": "{model} · {date}"
|
|
357
454
|
},
|
|
455
|
+
"memory": {
|
|
456
|
+
"enabled": true,
|
|
457
|
+
"dir": ".bylane/memory"
|
|
458
|
+
},
|
|
358
459
|
"extensions": {
|
|
359
460
|
"figma": { "enabled": false, "useAt": "issue-analysis" }
|
|
360
461
|
}
|
|
@@ -1,13 +1,38 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bylane-code-agent
|
|
3
|
-
description: issue-agent의 스펙을 기반으로
|
|
3
|
+
description: issue-agent의 전략 스펙을 기반으로 코드를 구현한다.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Code Agent
|
|
7
7
|
|
|
8
|
-
## 입력
|
|
8
|
+
## 입력 읽기
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
### 1. issue-agent 상태에서 spec 로드
|
|
11
|
+
|
|
12
|
+
`.bylane/state/issue-agent.json`에서 다음 필드를 읽는다:
|
|
13
|
+
|
|
14
|
+
| 필드 | 용도 |
|
|
15
|
+
|------|------|
|
|
16
|
+
| `spec.title` | 작업 제목 |
|
|
17
|
+
| `spec.approach` | 채택된 구현 방향 |
|
|
18
|
+
| `spec.affectedFiles` | 수정/추가할 파일 목록 |
|
|
19
|
+
| `spec.checklist` | 구현 체크리스트 |
|
|
20
|
+
| `spec.figmaSpec` | Figma 컬러토큰·컴포넌트 구조 |
|
|
21
|
+
| `issueType` | `new-feature` / `bug` / `improvement` / `chore` |
|
|
22
|
+
|
|
23
|
+
없으면 GitHub 이슈 본문을 직접 로드하여 "구현 방향", "관련 파일 및 영향 범위", "구현 체크리스트" 섹션을 파싱한다.
|
|
24
|
+
|
|
25
|
+
### 2. issueMemory 로드
|
|
26
|
+
|
|
27
|
+
이슈 번호를 알고 있으면 이전 세션 컨텍스트를 확인한다:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx @elyun/bylane memory read ISSUE_NUMBER
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
아키텍처 결정이나 트러블슈팅 기록이 있으면 구현에 반영한다.
|
|
34
|
+
|
|
35
|
+
---
|
|
11
36
|
|
|
12
37
|
## 실행 전 상태 기록
|
|
13
38
|
|
|
@@ -15,30 +40,69 @@ description: issue-agent의 스펙을 기반으로 프론트엔드 코드를 구
|
|
|
15
40
|
npx @elyun/bylane state write code-agent '{"status":"in_progress","startedAt":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","progress":0,"currentTask":"코드 구현 시작","retries":0,"log":[]}'
|
|
16
41
|
```
|
|
17
42
|
|
|
43
|
+
---
|
|
44
|
+
|
|
18
45
|
## 실행 흐름
|
|
19
46
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
47
|
+
### issueType별 접근 전략
|
|
48
|
+
|
|
49
|
+
#### `new-feature`
|
|
50
|
+
- `spec.affectedFiles`의 신규 파일부터 생성
|
|
51
|
+
- 유사 구현 패턴(이슈 본문 "코드 패턴 참고" 섹션) 따르기
|
|
52
|
+
- Figma 스펙이 있으면 컬러토큰 → CSS 변수/Tailwind config 변환
|
|
53
|
+
|
|
54
|
+
#### `bug`
|
|
55
|
+
- 이슈 본문의 "문제 지점" 파일부터 열기
|
|
56
|
+
- 원인 파악 후 최소 변경으로 수정
|
|
57
|
+
- 재현 케이스를 테스트로 먼저 작성 (TDD)
|
|
58
|
+
|
|
59
|
+
#### `improvement`
|
|
60
|
+
- 기존 구현 파악 후 변경 범위 최소화
|
|
61
|
+
- 사이드 이펙트 발생 가능 파일은 이슈 본문 "영향 범위" 기준으로 확인
|
|
62
|
+
|
|
63
|
+
#### `chore`
|
|
64
|
+
- 기능 변경 없음 확인 후 진행
|
|
65
|
+
- 설정 파일 변경 시 기존 동작 보존 여부 체크
|
|
66
|
+
|
|
67
|
+
### 구현 순서
|
|
68
|
+
|
|
69
|
+
1. `spec.checklist` 항목을 순서대로 처리
|
|
70
|
+
2. 각 파일 구현 완료 시 상태 로그 기록:
|
|
27
71
|
```bash
|
|
28
72
|
npx @elyun/bylane state append code-agent "FILENAME 구현 완료"
|
|
29
73
|
```
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
npx @elyun/bylane state write code-agent '{"status":"completed","progress":100,"currentTask":"구현 완료","retries":0,"changedFiles":CHANGED_FILES_ARRAY}'
|
|
33
|
-
```
|
|
74
|
+
3. 코드베이스 기존 패턴과 동일한 스타일 유지 (TypeScript, 테스트 위치, import 방식 등)
|
|
34
75
|
|
|
35
|
-
|
|
76
|
+
### 코딩 원칙
|
|
36
77
|
|
|
37
78
|
- 함수형 컴포넌트 + hooks 우선
|
|
38
79
|
- 파일당 단일 책임
|
|
39
80
|
- 200줄 초과 시 분리 고려
|
|
40
81
|
- 불변성 패턴 유지 (객체 직접 수정 금지)
|
|
41
82
|
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 완료 처리
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npx @elyun/bylane state write code-agent '{"status":"completed","progress":100,"currentTask":"구현 완료","retries":0,"changedFiles":CHANGED_FILES_ARRAY}'
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## issueMemory 기록
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npx @elyun/bylane memory append ISSUE_NUMBER code-agent "구현 요약: SUMMARY
|
|
97
|
+
변경 파일: CHANGED_FILES
|
|
98
|
+
아키텍처 결정: DECISIONS
|
|
99
|
+
트러블슈팅: ISSUES_FACED"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`memory.enabled: false`이면 생략.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
42
106
|
## 출력
|
|
43
107
|
|
|
44
108
|
`.bylane/state/code-agent.json`의 `changedFiles`: 변경된 파일 경로 배열
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bylane-issue-agent
|
|
3
|
-
description:
|
|
3
|
+
description: 코드베이스 분석 + 사용자 문답으로 전략을 수립하고 GitHub 이슈를 작성한다.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Issue Agent
|
|
7
7
|
|
|
8
|
+
## 역할
|
|
9
|
+
|
|
10
|
+
코드베이스를 병렬로 분석하고, 사용자와 문답을 거쳐 이슈 유형을 분류한 뒤,
|
|
11
|
+
code-agent가 방향을 명확하게 읽을 수 있는 구조화된 이슈를 작성한다.
|
|
12
|
+
|
|
8
13
|
## GitHub 접근 방법
|
|
9
14
|
|
|
10
|
-
`.bylane/bylane.json`의 `github.method
|
|
15
|
+
`.bylane/bylane.json`의 `github.method`:
|
|
11
16
|
|
|
12
17
|
| 값 | 동작 |
|
|
13
18
|
|----|------|
|
|
@@ -16,69 +21,195 @@ description: GitHub Issue 생성 및 분석. Figma 링크 감지 시 스펙 추
|
|
|
16
21
|
| `"api"` | REST API + `$GITHUB_TOKEN` |
|
|
17
22
|
| `"auto"` (기본) | MCP → CLI → API 순서로 시도 |
|
|
18
23
|
|
|
19
|
-
##
|
|
24
|
+
## 상태 기록 (시작 시)
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx @elyun/bylane state write issue-agent '{"status":"in_progress","startedAt":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","progress":0,"currentTask":"전략 수립 중","retries":0,"log":[]}'
|
|
28
|
+
```
|
|
20
29
|
|
|
21
|
-
|
|
30
|
+
---
|
|
22
31
|
|
|
23
32
|
## 실행 흐름
|
|
24
33
|
|
|
25
|
-
###
|
|
34
|
+
### Phase 0 — issueMemory 로드 (이슈 번호가 있는 경우)
|
|
35
|
+
|
|
36
|
+
기존 이슈 번호가 입력되었으면 이전 작업 컨텍스트를 불러온다:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx @elyun/bylane memory read ISSUE_NUMBER
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
출력된 내용(이전 아키텍처 결정, 트러블슈팅 기록)을 이후 분석에 반영한다.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
### Phase 1 — 이슈 유형 1차 분류
|
|
47
|
+
|
|
48
|
+
입력 텍스트 또는 기존 이슈 본문을 분석하여 유형을 예비 분류:
|
|
49
|
+
|
|
50
|
+
| 유형 | 판단 기준 |
|
|
51
|
+
|------|-----------|
|
|
52
|
+
| `new-feature` | 신규 컴포넌트/페이지/기능 추가 |
|
|
53
|
+
| `bug` | 오류, 크래시, 잘못된 동작 |
|
|
54
|
+
| `improvement` | 기존 기능 수정·개선·리팩토링 |
|
|
55
|
+
| `chore` | 설정, 의존성, 빌드, 문서 |
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
### Phase 2 — 코드베이스 병렬 분석
|
|
60
|
+
|
|
61
|
+
서브에이전트 3개를 **동시에** 실행한다. 각 에이전트는 아래 질문에 답한다.
|
|
62
|
+
|
|
63
|
+
#### 서브에이전트 A — 관련 파일 탐색
|
|
64
|
+
- 입력 의도와 관련된 파일·폴더를 DFS로 탐색
|
|
65
|
+
- 관련 컴포넌트, hook, util, 타입 정의 목록화
|
|
66
|
+
- 현재 구현 방식 요약 (있는 경우)
|
|
67
|
+
|
|
68
|
+
#### 서브에이전트 B — 코드 패턴 분석
|
|
69
|
+
- 유사 기능의 구현 패턴 샘플링 (컴포넌트 구조, 상태 관리, API 호출 방식)
|
|
70
|
+
- 네이밍 컨벤션, 파일 분리 방식
|
|
71
|
+
- 테스트 파일 위치 및 작성 패턴
|
|
72
|
+
|
|
73
|
+
#### 서브에이전트 C — 의존성 및 영향 범위
|
|
74
|
+
- 변경 시 영향받는 파일·모듈 목록
|
|
75
|
+
- 공유 컴포넌트/훅 여부 확인
|
|
76
|
+
- `bug` / `improvement` 유형이면 해당 코드의 현재 상태와 문제 지점 파악
|
|
77
|
+
|
|
78
|
+
**`extensions.figma.enabled === true`이고 Figma URL이 있는 경우:**
|
|
79
|
+
서브에이전트 A와 병렬로 Figma MCP 분석도 실행:
|
|
80
|
+
- `get_file` / `get_node`로 프레임/컴포넌트 구조 추출
|
|
81
|
+
- 컬러 토큰, 타이포그래피, 레이아웃 정보 추출
|
|
82
|
+
- 실패 시 경고 로그 후 텍스트 기반 스펙으로 fallback
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
### Phase 3 — 사용자 문답 (방향 확정)
|
|
87
|
+
|
|
88
|
+
병렬 분석 결과를 요약하여 사용자에게 제시하고, **핵심 결정 사항만** 질문한다.
|
|
89
|
+
|
|
90
|
+
질문 예시 (유형에 따라 선택):
|
|
91
|
+
|
|
92
|
+
**new-feature:**
|
|
93
|
+
```
|
|
94
|
+
분석 결과:
|
|
95
|
+
- 관련 파일: src/components/theme/, src/hooks/useTheme.ts
|
|
96
|
+
- 유사 구현: ColorPicker 컴포넌트 (src/components/ColorPicker/)
|
|
97
|
+
|
|
98
|
+
결정이 필요한 사항:
|
|
99
|
+
1. 토글 위치: 헤더 우측 / 사이드바 하단 / 플로팅 버튼?
|
|
100
|
+
2. 상태 저장: localStorage / 서버 동기화?
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**bug:**
|
|
104
|
+
```
|
|
105
|
+
분석 결과:
|
|
106
|
+
- 문제 지점: src/hooks/useAuth.ts:47 — 토큰 만료 시 갱신 로직 누락
|
|
107
|
+
- 영향 범위: useAuth를 사용하는 12개 컴포넌트
|
|
108
|
+
|
|
109
|
+
확인 사항:
|
|
110
|
+
1. 재현 조건이 있으신가요? (특정 브라우저, 로그인 상태 등)
|
|
111
|
+
2. 임시 수정 vs 근본 해결 중 어느 방향으로 진행할까요?
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
문답은 **1~3개 질문**으로 제한. 명확한 경우 생략 가능.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
### Phase 4 — 이슈 작성
|
|
119
|
+
|
|
120
|
+
문답 결과와 분석 내용을 바탕으로 GitHub 이슈를 작성한다.
|
|
121
|
+
|
|
122
|
+
#### 이슈 본문 구조
|
|
123
|
+
|
|
124
|
+
```markdown
|
|
125
|
+
## 개요
|
|
126
|
+
|
|
127
|
+
[한 줄 요약 — 무엇을, 왜]
|
|
128
|
+
|
|
129
|
+
**유형:** `new-feature` | `bug` | `improvement` | `chore`
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 배경 및 목적
|
|
134
|
+
|
|
135
|
+
[사용자 의도 + 분석으로 파악한 현재 상태]
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 구현 방향
|
|
140
|
+
|
|
141
|
+
[Phase 3 문답에서 확정된 전략적 방향]
|
|
142
|
+
|
|
143
|
+
- 접근 방법: ...
|
|
144
|
+
- 채택 이유: ...
|
|
145
|
+
- 제외한 대안: ... (이유)
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## 관련 파일 및 영향 범위
|
|
150
|
+
|
|
151
|
+
| 파일/모듈 | 역할 | 변경 필요 여부 |
|
|
152
|
+
|-----------|------|--------------|
|
|
153
|
+
| `src/hooks/useTheme.ts` | 테마 상태 관리 | 수정 |
|
|
154
|
+
| `src/components/Header/` | 토글 위치 | 추가 |
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 코드 패턴 참고
|
|
26
159
|
|
|
27
|
-
|
|
28
|
-
- 제목 (50자 이내), 상세 설명, 구현 체크리스트, Figma URL
|
|
160
|
+
[서브에이전트 B가 발견한 유사 구현 패턴 요약]
|
|
29
161
|
|
|
30
|
-
|
|
162
|
+
```typescript
|
|
163
|
+
// 유사 구현 예시 (src/components/ColorPicker/index.tsx 참조)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
31
167
|
|
|
32
|
-
|
|
33
|
-
→ GitHub MCP `create_issue` 도구 사용
|
|
168
|
+
## Figma 스펙 (해당 시)
|
|
34
169
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
--title "TITLE" \
|
|
39
|
-
--body "BODY" \
|
|
40
|
-
--label "bylane-auto"
|
|
41
|
-
```
|
|
170
|
+
- 컴포넌트: [컴포넌트명]
|
|
171
|
+
- 컬러 토큰: `--color-primary: #3B82F6`
|
|
172
|
+
- 레이아웃: [설명]
|
|
42
173
|
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
curl -s -X POST \
|
|
46
|
-
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
|
47
|
-
-H "Content-Type: application/json" \
|
|
48
|
-
https://api.github.com/repos/OWNER/REPO/issues \
|
|
49
|
-
-d '{"title":"TITLE","body":"BODY","labels":["bylane-auto"]}'
|
|
50
|
-
```
|
|
174
|
+
---
|
|
51
175
|
|
|
52
|
-
|
|
53
|
-
- `true`이고 Figma URL 있으면 → Figma 분석 단계 실행
|
|
54
|
-
- `false` → 텍스트 기반 스펙만 생성
|
|
176
|
+
## 구현 체크리스트
|
|
55
177
|
|
|
56
|
-
|
|
178
|
+
- [ ] [첫 번째 구현 단위]
|
|
179
|
+
- [ ] [두 번째 구현 단위]
|
|
180
|
+
- [ ] 테스트 작성
|
|
181
|
+
- [ ] 기존 테스트 통과 확인
|
|
57
182
|
|
|
58
|
-
|
|
183
|
+
---
|
|
59
184
|
|
|
60
|
-
|
|
61
|
-
→ GitHub MCP `get_issue` 도구 사용
|
|
185
|
+
## 주의사항
|
|
62
186
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
gh issue view NUMBER --json title,body,labels
|
|
66
|
-
```
|
|
187
|
+
[영향 범위, 사이드 이펙트, 알려진 제약]
|
|
188
|
+
```
|
|
67
189
|
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
curl -s \
|
|
71
|
-
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
|
72
|
-
https://api.github.com/repos/OWNER/REPO/issues/NUMBER
|
|
73
|
-
```
|
|
190
|
+
#### 이슈 생성
|
|
74
191
|
|
|
75
|
-
|
|
192
|
+
**MCP:**
|
|
193
|
+
→ GitHub MCP `create_issue` 도구 사용
|
|
76
194
|
|
|
77
|
-
|
|
195
|
+
**CLI:**
|
|
196
|
+
```bash
|
|
197
|
+
gh issue create \
|
|
198
|
+
--title "TITLE" \
|
|
199
|
+
--body "BODY" \
|
|
200
|
+
--label "bylane-auto"
|
|
201
|
+
```
|
|
78
202
|
|
|
79
|
-
|
|
203
|
+
**API:**
|
|
204
|
+
```bash
|
|
205
|
+
curl -s -X POST \
|
|
206
|
+
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
|
207
|
+
-H "Content-Type: application/json" \
|
|
208
|
+
https://api.github.com/repos/OWNER/REPO/issues \
|
|
209
|
+
-d '{"title":"TITLE","body":"BODY","labels":["bylane-auto"]}'
|
|
210
|
+
```
|
|
80
211
|
|
|
81
|
-
|
|
212
|
+
---
|
|
82
213
|
|
|
83
214
|
## 출력
|
|
84
215
|
|
|
@@ -91,20 +222,35 @@ Figma MCP `get_file` 또는 `get_node` 도구로 프레임/컴포넌트 분석.
|
|
|
91
222
|
"progress": 100,
|
|
92
223
|
"issueNumber": 123,
|
|
93
224
|
"issueUrl": "https://github.com/...",
|
|
225
|
+
"issueType": "new-feature",
|
|
94
226
|
"spec": {
|
|
95
227
|
"title": "다크모드 토글 버튼 추가",
|
|
96
228
|
"description": "...",
|
|
97
|
-
"
|
|
229
|
+
"approach": "localStorage 기반 useTheme hook 확장",
|
|
230
|
+
"affectedFiles": ["src/hooks/useTheme.ts", "src/components/Header/"],
|
|
231
|
+
"checklist": ["ThemeToggle 컴포넌트 생성", "useTheme hook 수정", "테스트 작성"],
|
|
98
232
|
"figmaSpec": { "enabled": false, "components": [], "colorTokens": {} }
|
|
99
233
|
}
|
|
100
234
|
}
|
|
101
235
|
```
|
|
102
236
|
|
|
103
|
-
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## issueMemory 기록
|
|
240
|
+
|
|
241
|
+
작업 완료 후:
|
|
242
|
+
|
|
104
243
|
```bash
|
|
105
|
-
npx @elyun/bylane
|
|
244
|
+
npx @elyun/bylane memory append ISSUE_NUMBER issue-agent "유형: ISSUE_TYPE
|
|
245
|
+
방향: APPROACH
|
|
246
|
+
관련 파일: AFFECTED_FILES
|
|
247
|
+
특이사항: NOTES"
|
|
106
248
|
```
|
|
107
249
|
|
|
250
|
+
`memory.enabled: false`이면 생략.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
108
254
|
## 수동 실행
|
|
109
255
|
|
|
110
256
|
`/bylane issue #123` 또는 `/bylane issue 다크모드 토글 추가해줘`
|
|
@@ -1,57 +1,62 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bylane-orchestrator
|
|
3
|
-
description: byLane 메인 오케스트레이터.
|
|
3
|
+
description: byLane 메인 오케스트레이터. 전략 수립 후 에이전트 파이프라인을 실행한다.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# byLane Orchestrator
|
|
7
7
|
|
|
8
8
|
## 역할
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
사용자 의도를 파싱하고, issue-agent를 통해 전략을 수립한 뒤 에이전트 파이프라인을 실행한다.
|
|
11
11
|
|
|
12
12
|
## 실행 전 체크
|
|
13
13
|
|
|
14
|
-
1. 사전
|
|
14
|
+
1. 사전 점검:
|
|
15
15
|
```bash
|
|
16
16
|
npx @elyun/bylane preflight
|
|
17
17
|
```
|
|
18
|
-
|
|
19
|
-
- `.bylane/bylane.json` 없으면 즉시 `bylane-setup` 스킬 실행.
|
|
18
|
+
실패 시 안내 메시지 출력 후 **중단**. `.bylane/bylane.json` 없으면 즉시 `bylane-setup` 스킬 실행.
|
|
20
19
|
|
|
21
20
|
2. `.bylane/state/` 디렉토리 확인. 없으면 생성.
|
|
22
21
|
|
|
23
22
|
## 에이전트별 모델 결정
|
|
24
23
|
|
|
25
|
-
각 에이전트 실행 전 사용할 모델을 config에서 읽는다:
|
|
26
|
-
|
|
27
24
|
```bash
|
|
28
25
|
npx @elyun/bylane models
|
|
29
26
|
```
|
|
30
27
|
|
|
31
|
-
출력 형식: `AGENT_NAME=MODEL_ID` (한 줄씩)
|
|
32
|
-
|
|
33
|
-
에이전트 호출 시 해당 모델을 `model` 파라미터로 전달한다.
|
|
28
|
+
출력 형식: `AGENT_NAME=MODEL_ID` (한 줄씩). 에이전트 호출 시 `model` 파라미터로 전달.
|
|
34
29
|
|
|
35
30
|
## 의도 파싱 규칙
|
|
36
31
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
| 패턴 | 실행할 에이전트 체인 |
|
|
32
|
+
| 패턴 | 실행 흐름 |
|
|
40
33
|
|---|---|
|
|
41
|
-
| "구현", "만들어", "추가해", 이슈 없음 | issue-agent → code-agent → test-agent → commit-agent → pr-agent → review-agent → notify-agent |
|
|
42
|
-
| "issue #N 구현", "이슈 #N 작업" |
|
|
34
|
+
| "구현", "만들어", "추가해", 이슈 없음 | **전략 수립** → issue-agent → code-agent → test-agent → commit-agent → pr-agent → review-agent → notify-agent |
|
|
35
|
+
| "issue #N 구현", "이슈 #N 작업" | **전략 수립(기존 이슈 분석)** → code-agent → test-agent → commit-agent → pr-agent → review-agent → notify-agent |
|
|
43
36
|
| "PR #N 리뷰", "리뷰해줘" | review-agent(PR번호 전달) |
|
|
44
|
-
| "리뷰 #N 반영", "리뷰 수락" | respond-agent(PR번호, 모드=accept
|
|
45
|
-
| "리뷰 #N 반박" | respond-agent(PR번호, 모드=rebut
|
|
37
|
+
| "리뷰 #N 반영", "리뷰 수락" | respond-agent(PR번호, 모드=accept) |
|
|
38
|
+
| "리뷰 #N 반박" | respond-agent(PR번호, 모드=rebut) |
|
|
46
39
|
| "커밋해줘" | commit-agent |
|
|
47
40
|
| "PR 만들어줘" | pr-agent |
|
|
48
41
|
| "테스트해줘" | test-agent |
|
|
49
|
-
| "프로젝트 분석", "analyze"
|
|
42
|
+
| "프로젝트 분석", "analyze" | analyze-agent |
|
|
43
|
+
|
|
44
|
+
## 전략 수립 단계 (새 기능 / 이슈 구현 시 필수)
|
|
45
|
+
|
|
46
|
+
`리뷰`, `커밋`, `PR`, `테스트` 단독 요청이 아닌 경우 반드시 전략 수립 후 진행.
|
|
47
|
+
|
|
48
|
+
issue-agent를 `model` 파라미터와 함께 호출한다. issue-agent 내부에서 다음을 수행:
|
|
49
|
+
|
|
50
|
+
1. 코드베이스 병렬 분석 (서브에이전트)
|
|
51
|
+
2. 사용자 문답
|
|
52
|
+
3. 이슈 분류 및 작성
|
|
53
|
+
|
|
54
|
+
상세 로직은 `bylane-issue-agent` 참조.
|
|
50
55
|
|
|
51
56
|
## 에이전트 실행 방법
|
|
52
57
|
|
|
53
|
-
각 에이전트를 순서대로 Agent 도구로
|
|
54
|
-
**config에서 읽은 모델을 `model` 파라미터로 반드시
|
|
58
|
+
각 에이전트를 순서대로 Agent 도구로 호출. 이전 출력을 다음 입력으로 전달.
|
|
59
|
+
**config에서 읽은 모델을 `model` 파라미터로 반드시 전달.**
|
|
55
60
|
|
|
56
61
|
상태 기록 (각 에이전트 시작 전):
|
|
57
62
|
```bash
|
|
@@ -60,17 +65,15 @@ npx @elyun/bylane state write AGENT_NAME '{"status":"in_progress","startedAt":"'
|
|
|
60
65
|
|
|
61
66
|
## 피드백 루프
|
|
62
67
|
|
|
63
|
-
test-agent가 FAIL
|
|
68
|
+
test-agent가 FAIL 반환 시:
|
|
64
69
|
1. `.bylane/state/test-agent.json`에서 `failureDetails` 읽기
|
|
65
|
-
2.
|
|
66
|
-
3. `retries
|
|
67
|
-
4. `retries >= maxRetries`이면 notify-agent에 "개입 필요" 메시지 전송 후 중단
|
|
70
|
+
2. `retries < config.workflow.maxRetries` → code-agent 재실행 (실패 피드백 포함, retries+1)
|
|
71
|
+
3. `retries >= maxRetries` → notify-agent에 "개입 필요" 메시지 후 중단
|
|
68
72
|
|
|
69
|
-
respond-agent가 "수정 필요"
|
|
73
|
+
respond-agent가 "수정 필요" 반환 시 동일 로직 적용.
|
|
70
74
|
|
|
71
75
|
## 완료 처리
|
|
72
76
|
|
|
73
|
-
모든 에이전트 완료 후:
|
|
74
77
|
1. 각 에이전트 state를 `status: "completed"`로 업데이트
|
|
75
78
|
2. notify-agent 실행하여 최종 결과 전송
|
|
76
79
|
|
package/commands/bylane-setup.md
CHANGED
|
@@ -6,7 +6,7 @@ description: byLane 하네스 최초 설치 및 설정 위자드. /bylane setup
|
|
|
6
6
|
# byLane Setup Wizard
|
|
7
7
|
|
|
8
8
|
사용자에게 단계별로 질문하여 `.bylane/bylane.json`을 생성한다.
|
|
9
|
-
ALWAYS complete all
|
|
9
|
+
ALWAYS complete all 8 steps before saving. NEVER skip steps.
|
|
10
10
|
|
|
11
11
|
## 실행 전 준비
|
|
12
12
|
|
|
@@ -83,7 +83,32 @@ Linear API Key는 환경변수명(`LINEAR_API_KEY`)으로 저장. 실제 키값
|
|
|
83
83
|
|
|
84
84
|
`permissions.scope`에 저장.
|
|
85
85
|
|
|
86
|
-
## Step 5/
|
|
86
|
+
## Step 5/8 — Loop 실행 모드
|
|
87
|
+
|
|
88
|
+
> Loop 실행 모드를 선택하세요:
|
|
89
|
+
> 1. tmux — tmux 세션에서 백그라운드 실행 (터미널 종료 후에도 유지, 권장)
|
|
90
|
+
> 2. process — 현재 프로세스에서 직접 실행 (잠자기 모드 대응 포함)
|
|
91
|
+
|
|
92
|
+
- `1` → `loop.mode = "tmux"`, `loop.sessionName` 입력 (Enter = `bylane-loops`)
|
|
93
|
+
- `2` → `loop.mode = "process"`
|
|
94
|
+
|
|
95
|
+
tmux 선택 시 `which tmux`로 설치 여부를 자동 확인한다:
|
|
96
|
+
- 설치됨 → `tmux` 모드 확정
|
|
97
|
+
- 미설치 → 안내 후 `process` 모드로 자동 전환:
|
|
98
|
+
> tmux가 설치되어 있지 않습니다. process 모드로 설정합니다.
|
|
99
|
+
> tmux 설치: `brew install tmux`
|
|
100
|
+
|
|
101
|
+
폴링 주기 입력 (Enter = 5분):
|
|
102
|
+
- `loop.intervalMs`에 저장 (밀리초)
|
|
103
|
+
|
|
104
|
+
loop 시작/종료 명령 안내:
|
|
105
|
+
```
|
|
106
|
+
bylane loop start # loop 시작
|
|
107
|
+
bylane loop stop # loop 종료
|
|
108
|
+
bylane loop status # 상태 확인
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Step 6/8 — 고급 설정
|
|
87
112
|
|
|
88
113
|
> 고급 설정을 변경하시겠습니까? (Enter = 기본값 사용)
|
|
89
114
|
|
|
@@ -92,7 +117,7 @@ Linear API Key는 환경변수명(`LINEAR_API_KEY`)으로 저장. 실제 키값
|
|
|
92
117
|
- `loopTimeoutMinutes` (기본: 30): 루프 타임아웃 (분)
|
|
93
118
|
- Figma MCP 활성화? (y/n, 기본: n)
|
|
94
119
|
|
|
95
|
-
## Step
|
|
120
|
+
## Step 7/8 — 브랜치 네이밍
|
|
96
121
|
|
|
97
122
|
> 브랜치 네이밍 패턴을 선택하세요:
|
|
98
123
|
> 1. {tracker}-{issue-number} 예) issues-32
|
|
@@ -107,7 +132,7 @@ Linear API Key는 환경변수명(`LINEAR_API_KEY`)으로 저장. 실제 키값
|
|
|
107
132
|
직접 입력 시 사용 가능한 토큰 목록 안내:
|
|
108
133
|
`{tracker}`, `{type}`, `{issue-number}`, `{custom-id}`, `{title-slug}`, `{date}`, `{username}`
|
|
109
134
|
|
|
110
|
-
## Step
|
|
135
|
+
## Step 8/8 — 에이전트 모델 설정
|
|
111
136
|
|
|
112
137
|
> 각 에이전트에 사용할 AI 모델을 설정하시겠습니까? (Enter = 기본값 사용)
|
|
113
138
|
|
|
@@ -147,3 +172,9 @@ Linear API Key는 환경변수명(`LINEAR_API_KEY`)으로 저장. 실제 키값
|
|
|
147
172
|
모니터 대시보드: npm run monitor (byLane 디렉토리에서)
|
|
148
173
|
또는: /bylane monitor
|
|
149
174
|
```
|
|
175
|
+
5. Loop 실행 안내:
|
|
176
|
+
```
|
|
177
|
+
Loop 시작: npx @elyun/bylane loop start
|
|
178
|
+
Loop 종료: npx @elyun/bylane loop stop
|
|
179
|
+
Loop 상태: npx @elyun/bylane loop status
|
|
180
|
+
```
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -185,6 +185,107 @@ if (command === 'install') {
|
|
|
185
185
|
const result = runCleanup()
|
|
186
186
|
console.log(formatCleanupResult(result))
|
|
187
187
|
console.log('\n 완료.\n')
|
|
188
|
+
} else if (command === 'memory') {
|
|
189
|
+
// memory read ISSUE_NUMBER
|
|
190
|
+
// memory append ISSUE_NUMBER AGENT_NAME "content"
|
|
191
|
+
// memory list
|
|
192
|
+
const subCmd = args[1]
|
|
193
|
+
const { loadConfig } = await import('./config.js')
|
|
194
|
+
const { readIssueMemory, appendIssueMemory, listIssueMemories } = await import('./memory.js')
|
|
195
|
+
const config = loadConfig()
|
|
196
|
+
|
|
197
|
+
if (subCmd === 'read') {
|
|
198
|
+
const issueNumber = args[2]
|
|
199
|
+
if (!issueNumber) { console.error('사용법: bylane memory read <issueNumber>'); process.exit(1) }
|
|
200
|
+
const content = readIssueMemory(issueNumber, config)
|
|
201
|
+
console.log(content ?? '(메모리 없음)')
|
|
202
|
+
} else if (subCmd === 'append') {
|
|
203
|
+
const issueNumber = args[2]
|
|
204
|
+
const agentName = args[3]
|
|
205
|
+
const content = args[4]
|
|
206
|
+
if (!issueNumber || !agentName || !content) {
|
|
207
|
+
console.error('사용법: bylane memory append <issueNumber> <agentName> "content"')
|
|
208
|
+
process.exit(1)
|
|
209
|
+
}
|
|
210
|
+
appendIssueMemory(issueNumber, agentName, content, config)
|
|
211
|
+
console.log(` + Issue #${issueNumber} 메모리 기록 완료`)
|
|
212
|
+
} else if (subCmd === 'list') {
|
|
213
|
+
const issues = listIssueMemories(config)
|
|
214
|
+
if (issues.length === 0) {
|
|
215
|
+
console.log(' (기록된 이슈 메모리 없음)')
|
|
216
|
+
} else {
|
|
217
|
+
console.log(' 기록된 이슈 메모리:')
|
|
218
|
+
issues.forEach(n => console.log(` - Issue #${n}`))
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
console.error('사용법: bylane memory <read|append|list> [issueNumber] [agentName] [content]')
|
|
222
|
+
process.exit(1)
|
|
223
|
+
}
|
|
224
|
+
} else if (command === 'loop') {
|
|
225
|
+
const subCmd = args[1] || 'start'
|
|
226
|
+
const { resolveLoopMode, startTmuxLoops, stopTmuxLoops, isTmuxSessionAlive } = await import('./loop-utils.js')
|
|
227
|
+
const { loadConfig } = await import('./config.js')
|
|
228
|
+
const config = loadConfig()
|
|
229
|
+
const sessionName = config.loop?.sessionName ?? 'bylane-loops'
|
|
230
|
+
|
|
231
|
+
if (subCmd === 'start') {
|
|
232
|
+
const mode = resolveLoopMode()
|
|
233
|
+
|
|
234
|
+
if (mode === 'tmux') {
|
|
235
|
+
startTmuxLoops(sessionName)
|
|
236
|
+
console.log(`\n tmux 세션에 접속하려면: tmux attach -t ${sessionName}\n`)
|
|
237
|
+
} else {
|
|
238
|
+
// process 모드: 현재 프로세스에서 직접 실행
|
|
239
|
+
console.log('\n process 모드: review-loop + respond-loop 실행\n')
|
|
240
|
+
const { spawn } = await import('child_process')
|
|
241
|
+
const review = spawn(process.execPath, ['src/review-loop.js'], { stdio: 'inherit', detached: false })
|
|
242
|
+
const respond = spawn(process.execPath, ['src/respond-loop.js'], { stdio: 'inherit', detached: false })
|
|
243
|
+
|
|
244
|
+
function shutdownAll() {
|
|
245
|
+
review.kill('SIGTERM')
|
|
246
|
+
respond.kill('SIGTERM')
|
|
247
|
+
process.exit(0)
|
|
248
|
+
}
|
|
249
|
+
process.on('SIGINT', shutdownAll)
|
|
250
|
+
process.on('SIGTERM', shutdownAll)
|
|
251
|
+
|
|
252
|
+
// 자식 프로세스가 모두 종료되면 부모도 종료
|
|
253
|
+
let exited = 0
|
|
254
|
+
const onExit = () => { if (++exited >= 2) process.exit(0) }
|
|
255
|
+
review.on('exit', onExit)
|
|
256
|
+
respond.on('exit', onExit)
|
|
257
|
+
}
|
|
258
|
+
} else if (subCmd === 'stop') {
|
|
259
|
+
const mode = resolveLoopMode()
|
|
260
|
+
if (mode === 'tmux') {
|
|
261
|
+
stopTmuxLoops(sessionName)
|
|
262
|
+
} else {
|
|
263
|
+
// process 모드: state에서 PID를 읽어 종료
|
|
264
|
+
const { readState } = await import('./state.js')
|
|
265
|
+
for (const loopName of ['review-loop', 'respond-loop']) {
|
|
266
|
+
const state = readState(loopName)
|
|
267
|
+
if (state?.pid) {
|
|
268
|
+
try {
|
|
269
|
+
process.kill(state.pid, 'SIGTERM')
|
|
270
|
+
console.log(` ${loopName} (PID: ${state.pid}) 종료`)
|
|
271
|
+
} catch {
|
|
272
|
+
console.log(` ${loopName} (PID: ${state.pid}) 이미 종료됨`)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
} else if (subCmd === 'status') {
|
|
278
|
+
const alive = isTmuxSessionAlive(sessionName)
|
|
279
|
+
const { readState } = await import('./state.js')
|
|
280
|
+
const reviewState = readState('review-loop')
|
|
281
|
+
const respondState = readState('respond-loop')
|
|
282
|
+
console.log(`\n tmux 세션 (${sessionName}): ${alive ? '실행 중' : '없음'}`)
|
|
283
|
+
console.log(` review-loop: ${reviewState?.status ?? 'unknown'}`)
|
|
284
|
+
console.log(` respond-loop: ${respondState?.status ?? 'unknown'}\n`)
|
|
285
|
+
} else {
|
|
286
|
+
console.error('사용법: bylane loop <start|stop|status>')
|
|
287
|
+
process.exit(1)
|
|
288
|
+
}
|
|
188
289
|
} else if (command === 'monitor') {
|
|
189
290
|
// 항상 현재 패키지의 모니터 실행 (버전 일치 보장)
|
|
190
291
|
const monitorPath = join(__dirname, 'monitor', 'index.js')
|
|
@@ -193,6 +294,6 @@ if (command === 'install') {
|
|
|
193
294
|
child.on('exit', code => process.exit(code ?? 0))
|
|
194
295
|
} else {
|
|
195
296
|
console.error(`알 수 없는 명령: ${command}`)
|
|
196
|
-
console.error('사용법: npx @elyun/bylane [install|monitor] [--symlink]')
|
|
297
|
+
console.error('사용법: npx @elyun/bylane [install|loop|monitor|preflight|state|memory|cleanup] [--symlink]')
|
|
197
298
|
process.exit(1)
|
|
198
299
|
}
|
package/src/config.js
CHANGED
|
@@ -40,6 +40,10 @@ export const DEFAULT_CONFIG = {
|
|
|
40
40
|
owner: '',
|
|
41
41
|
repo: ''
|
|
42
42
|
},
|
|
43
|
+
memory: {
|
|
44
|
+
enabled: true,
|
|
45
|
+
dir: '.bylane/memory'
|
|
46
|
+
},
|
|
43
47
|
models: {
|
|
44
48
|
default: 'claude-sonnet-4-6',
|
|
45
49
|
orchestrator: 'claude-opus-4-6',
|
|
@@ -53,6 +57,11 @@ export const DEFAULT_CONFIG = {
|
|
|
53
57
|
'notify-agent': 'claude-haiku-4-5-20251001',
|
|
54
58
|
'analyze-agent': 'claude-opus-4-6'
|
|
55
59
|
},
|
|
60
|
+
loop: {
|
|
61
|
+
mode: 'tmux',
|
|
62
|
+
intervalMs: 300000,
|
|
63
|
+
sessionName: 'bylane-loops'
|
|
64
|
+
},
|
|
56
65
|
review: {
|
|
57
66
|
model: 'claude-sonnet-4-6',
|
|
58
67
|
language: 'ko',
|
package/src/loop-utils.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { execSync } from 'child_process'
|
|
6
6
|
import { readState, writeState } from './state.js'
|
|
7
|
+
import { loadConfig } from './config.js'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* 동일 루프가 이미 실행 중이면 기존 프로세스를 종료하고 기다린다.
|
|
@@ -45,3 +46,104 @@ export function killExistingLoop(loopName, stateDir = '.bylane/state') {
|
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* tmux 세션이 살아있는지 확인
|
|
52
|
+
* @param {string} sessionName
|
|
53
|
+
* @returns {boolean}
|
|
54
|
+
*/
|
|
55
|
+
export function isTmuxSessionAlive(sessionName) {
|
|
56
|
+
try {
|
|
57
|
+
execSync(`tmux has-session -t ${sessionName}`, { stdio: ['pipe', 'pipe', 'pipe'] })
|
|
58
|
+
return true
|
|
59
|
+
} catch {
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* tmux 세션에서 review-loop + respond-loop을 실행
|
|
66
|
+
* @param {string} sessionName
|
|
67
|
+
*/
|
|
68
|
+
export function startTmuxLoops(sessionName = 'bylane-loops') {
|
|
69
|
+
if (isTmuxSessionAlive(sessionName)) {
|
|
70
|
+
console.log(`[tmux] 세션 '${sessionName}'이 이미 실행 중입니다.`)
|
|
71
|
+
return { started: false, reason: 'already_running' }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 첫 번째 윈도우: review-loop
|
|
75
|
+
execSync(
|
|
76
|
+
`tmux new-session -d -s ${sessionName} -n review 'node src/review-loop.js'`,
|
|
77
|
+
{ stdio: 'inherit' }
|
|
78
|
+
)
|
|
79
|
+
// 두 번째 윈도우: respond-loop
|
|
80
|
+
execSync(
|
|
81
|
+
`tmux new-window -t ${sessionName} -n respond 'node src/respond-loop.js'`,
|
|
82
|
+
{ stdio: 'inherit' }
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
console.log(`[tmux] 세션 '${sessionName}' 시작 완료 (review-loop + respond-loop)`)
|
|
86
|
+
return { started: true, sessionName }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* tmux 세션 종료
|
|
91
|
+
* @param {string} sessionName
|
|
92
|
+
*/
|
|
93
|
+
export function stopTmuxLoops(sessionName = 'bylane-loops') {
|
|
94
|
+
if (!isTmuxSessionAlive(sessionName)) {
|
|
95
|
+
console.log(`[tmux] 세션 '${sessionName}'이 존재하지 않습니다.`)
|
|
96
|
+
return { stopped: false, reason: 'not_running' }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
execSync(`tmux kill-session -t ${sessionName}`, { stdio: 'inherit' })
|
|
100
|
+
console.log(`[tmux] 세션 '${sessionName}' 종료 완료`)
|
|
101
|
+
return { stopped: true, sessionName }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 절대 시간 기반 폴링 타이머.
|
|
106
|
+
* setInterval과 달리 잠자기 모드 후 깨어났을 때 즉시 보정한다.
|
|
107
|
+
* @param {() => void | Promise<void>} fn 폴링 콜백
|
|
108
|
+
* @param {number} intervalMs 폴링 간격 (ms)
|
|
109
|
+
* @param {number} [checkMs=10000] 체크 주기 (ms)
|
|
110
|
+
* @returns {{ timer: NodeJS.Timeout, stop: () => void }}
|
|
111
|
+
*/
|
|
112
|
+
export function createAbsoluteTimer(fn, intervalMs, checkMs = 10000) {
|
|
113
|
+
let lastRun = Date.now()
|
|
114
|
+
|
|
115
|
+
const timer = setInterval(async () => {
|
|
116
|
+
const now = Date.now()
|
|
117
|
+
if (now - lastRun >= intervalMs) {
|
|
118
|
+
lastRun = now
|
|
119
|
+
await fn()
|
|
120
|
+
}
|
|
121
|
+
}, checkMs)
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
timer,
|
|
125
|
+
stop() { clearInterval(timer) }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 현재 설정에서 loop 모드를 결정한다.
|
|
131
|
+
* tmux 모드인데 tmux가 없으면 process로 fallback.
|
|
132
|
+
* @returns {'tmux' | 'process'}
|
|
133
|
+
*/
|
|
134
|
+
export function resolveLoopMode() {
|
|
135
|
+
const config = loadConfig()
|
|
136
|
+
const mode = config.loop?.mode ?? 'tmux'
|
|
137
|
+
|
|
138
|
+
if (mode === 'tmux') {
|
|
139
|
+
try {
|
|
140
|
+
execSync('which tmux', { stdio: ['pipe', 'pipe', 'pipe'] })
|
|
141
|
+
return 'tmux'
|
|
142
|
+
} catch {
|
|
143
|
+
console.log('[loop] tmux 미설치 — process 모드로 fallback합니다.')
|
|
144
|
+
return 'process'
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return 'process'
|
|
149
|
+
}
|
package/src/memory.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'fs'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { execSync } from 'child_process'
|
|
4
|
+
|
|
5
|
+
// 현재 실행 중인 루프가 있는지 확인 (review-loop, respond-loop)
|
|
6
|
+
function isAnyLoopActive(stateDir = '.bylane/state') {
|
|
7
|
+
if (!existsSync(stateDir)) return false
|
|
8
|
+
try {
|
|
9
|
+
return readdirSync(stateDir)
|
|
10
|
+
.filter(f => f.endsWith('-loop.json'))
|
|
11
|
+
.some(f => {
|
|
12
|
+
try {
|
|
13
|
+
const s = JSON.parse(readFileSync(join(stateDir, f), 'utf8'))
|
|
14
|
+
return s.status === 'running'
|
|
15
|
+
} catch { return false }
|
|
16
|
+
})
|
|
17
|
+
} catch { return false }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getIssueMemoryPath(issueNumber, config) {
|
|
21
|
+
const dir = config?.memory?.dir ?? '.bylane/memory'
|
|
22
|
+
return join(dir, 'issues', `${issueNumber}.md`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function readIssueMemory(issueNumber, config = {}) {
|
|
26
|
+
const path = getIssueMemoryPath(issueNumber, config)
|
|
27
|
+
if (!existsSync(path)) return null
|
|
28
|
+
try { return readFileSync(path, 'utf8') } catch { return null }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function appendIssueMemory(issueNumber, agentName, content, config = {}) {
|
|
32
|
+
if (config?.memory?.enabled === false) return
|
|
33
|
+
|
|
34
|
+
const timestamp = new Date().toISOString()
|
|
35
|
+
const entry = `\n## [${agentName}] ${timestamp}\n\n${content}\n`
|
|
36
|
+
|
|
37
|
+
if (isAnyLoopActive()) {
|
|
38
|
+
// 루프 실행 중 → GitHub 이슈 코멘트에 기록
|
|
39
|
+
postGitHubComment(issueNumber, entry, config)
|
|
40
|
+
} else {
|
|
41
|
+
// 루프 없음 → 로컬 파일에 기록
|
|
42
|
+
const path = getIssueMemoryPath(issueNumber, config)
|
|
43
|
+
const memDir = join(config?.memory?.dir ?? '.bylane/memory', 'issues')
|
|
44
|
+
mkdirSync(memDir, { recursive: true })
|
|
45
|
+
|
|
46
|
+
const existing = existsSync(path) ? readFileSync(path, 'utf8') : `# Issue #${issueNumber} Memory\n`
|
|
47
|
+
writeFileSync(path, existing + entry)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function postGitHubComment(issueNumber, content, config) {
|
|
52
|
+
try {
|
|
53
|
+
const method = config?.github?.method ?? 'auto'
|
|
54
|
+
const owner = config?.github?.owner ?? ''
|
|
55
|
+
const repo = config?.github?.repo ?? ''
|
|
56
|
+
|
|
57
|
+
if ((method === 'auto' || method === 'cli') && isCommandAvailable('gh')) {
|
|
58
|
+
const repoFlag = owner && repo ? `--repo ${owner}/${repo}` : ''
|
|
59
|
+
execSync(`gh issue comment ${issueNumber} ${repoFlag} --body ${JSON.stringify(content)}`, { stdio: 'ignore' })
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if ((method === 'auto' || method === 'api') && process.env.GITHUB_TOKEN) {
|
|
64
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/comments`
|
|
65
|
+
execSync(
|
|
66
|
+
`curl -s -X POST -H "Authorization: Bearer ${process.env.GITHUB_TOKEN}" -H "Content-Type: application/json" ${url} -d ${JSON.stringify(JSON.stringify({ body: content }))}`,
|
|
67
|
+
{ stdio: 'ignore' }
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
// GitHub 코멘트 실패 시 로컬 파일에 fallback
|
|
72
|
+
const path = getIssueMemoryPath(issueNumber, config)
|
|
73
|
+
const memDir = join(config?.memory?.dir ?? '.bylane/memory', 'issues')
|
|
74
|
+
mkdirSync(memDir, { recursive: true })
|
|
75
|
+
const existing = existsSync(path) ? readFileSync(path, 'utf8') : `# Issue #${issueNumber} Memory\n`
|
|
76
|
+
writeFileSync(path, existing + content)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isCommandAvailable(cmd) {
|
|
81
|
+
try { execSync(`which ${cmd}`, { stdio: 'ignore' }); return true } catch { return false }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function listIssueMemories(config = {}) {
|
|
85
|
+
const dir = join(config?.memory?.dir ?? '.bylane/memory', 'issues')
|
|
86
|
+
if (!existsSync(dir)) return []
|
|
87
|
+
try {
|
|
88
|
+
return readdirSync(dir)
|
|
89
|
+
.filter(f => f.endsWith('.md'))
|
|
90
|
+
.map(f => f.replace('.md', ''))
|
|
91
|
+
.sort((a, b) => Number(a) - Number(b))
|
|
92
|
+
} catch { return [] }
|
|
93
|
+
}
|
package/src/monitor/index.js
CHANGED
|
@@ -97,6 +97,14 @@ screen.key('s', () => {
|
|
|
97
97
|
list.focus()
|
|
98
98
|
screen.render()
|
|
99
99
|
|
|
100
|
+
// 명시적 키 바인딩 (screen-level 핸들러와 충돌 방지)
|
|
101
|
+
list.key(['up', 'k'], () => { list.up(1); screen.render() })
|
|
102
|
+
list.key(['down', 'j'], () => { list.down(1); screen.render() })
|
|
103
|
+
list.key('enter', () => {
|
|
104
|
+
const idx = list.selected
|
|
105
|
+
list.emit('select', list.items[idx], idx)
|
|
106
|
+
})
|
|
107
|
+
|
|
100
108
|
list.on('select', (item, idx) => {
|
|
101
109
|
const target = items[idx]
|
|
102
110
|
if (!target) { screen.remove(list); screen.render(); return }
|
package/src/preflight.js
CHANGED
|
@@ -17,6 +17,12 @@ function run(cmd) {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export function checkTmux() {
|
|
21
|
+
const which = run('which tmux')
|
|
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 }
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
function checkGhCli() {
|
|
21
27
|
const which = run('which gh')
|
|
22
28
|
if (!which.ok) return { ok: false, reason: 'gh CLI 미설치', fix: 'brew install gh # 또는 https://cli.github.com' }
|
|
@@ -105,14 +111,20 @@ export function runPreflight() {
|
|
|
105
111
|
}
|
|
106
112
|
}
|
|
107
113
|
|
|
108
|
-
// 3.
|
|
114
|
+
// 3. tmux (loop.mode가 tmux인 경우)
|
|
115
|
+
if (config.loop?.mode === 'tmux') {
|
|
116
|
+
const tmux = checkTmux()
|
|
117
|
+
results.push({ name: 'tmux', ...tmux, warn: !tmux.ok })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 4. 알림 채널 (설정된 경우만)
|
|
109
121
|
const slack = checkSlack(config)
|
|
110
122
|
if (slack) results.push({ name: 'Slack 알림', ...slack })
|
|
111
123
|
|
|
112
124
|
const telegram = checkTelegram(config)
|
|
113
125
|
if (telegram) results.push({ name: 'Telegram 알림', ...telegram })
|
|
114
126
|
|
|
115
|
-
const passed = results.every(r => r.ok)
|
|
127
|
+
const passed = results.filter(r => !r.warn).every(r => r.ok)
|
|
116
128
|
return { passed, results }
|
|
117
129
|
}
|
|
118
130
|
|
package/src/respond-loop.js
CHANGED
|
@@ -8,9 +8,10 @@ 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
|
+
import { killExistingLoop, createAbsoluteTimer } from './loop-utils.js'
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
const config = loadConfig()
|
|
14
|
+
const INTERVAL_MS = config.loop?.intervalMs ?? 300000
|
|
14
15
|
const STATE_DIR = '.bylane/state'
|
|
15
16
|
|
|
16
17
|
mkdirSync(STATE_DIR, { recursive: true })
|
|
@@ -173,20 +174,17 @@ async function poll() {
|
|
|
173
174
|
// 초기 실행
|
|
174
175
|
poll()
|
|
175
176
|
|
|
176
|
-
//
|
|
177
|
-
const
|
|
177
|
+
// 절대 시간 기반 폴링 (잠자기 모드 후 즉시 보정)
|
|
178
|
+
const { stop } = createAbsoluteTimer(poll, INTERVAL_MS)
|
|
178
179
|
|
|
179
180
|
// 종료 처리
|
|
180
|
-
|
|
181
|
-
|
|
181
|
+
function shutdown() {
|
|
182
|
+
stop()
|
|
182
183
|
writeState('respond-loop', { status: 'stopped' }, STATE_DIR)
|
|
183
184
|
process.exit(0)
|
|
184
|
-
}
|
|
185
|
-
process.on('
|
|
186
|
-
|
|
187
|
-
writeState('respond-loop', { status: 'stopped' }, STATE_DIR)
|
|
188
|
-
process.exit(0)
|
|
189
|
-
})
|
|
185
|
+
}
|
|
186
|
+
process.on('SIGINT', shutdown)
|
|
187
|
+
process.on('SIGTERM', shutdown)
|
|
190
188
|
|
|
191
189
|
writeState('respond-loop', { status: 'running', startedAt: new Date().toISOString(), pid: process.pid }, STATE_DIR)
|
|
192
190
|
console.log('respond-loop 시작. Ctrl+C로 종료.')
|
package/src/review-loop.js
CHANGED
|
@@ -7,9 +7,10 @@ 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
|
+
import { killExistingLoop, createAbsoluteTimer } from './loop-utils.js'
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const config = loadConfig()
|
|
13
|
+
const INTERVAL_MS = config.loop?.intervalMs ?? 300000
|
|
13
14
|
const STATE_DIR = '.bylane/state'
|
|
14
15
|
|
|
15
16
|
mkdirSync(STATE_DIR, { recursive: true })
|
|
@@ -120,20 +121,17 @@ async function poll() {
|
|
|
120
121
|
// 초기 실행
|
|
121
122
|
poll()
|
|
122
123
|
|
|
123
|
-
//
|
|
124
|
-
const
|
|
124
|
+
// 절대 시간 기반 폴링 (잠자기 모드 후 즉시 보정)
|
|
125
|
+
const { stop } = createAbsoluteTimer(poll, INTERVAL_MS)
|
|
125
126
|
|
|
126
127
|
// 종료 처리
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
function shutdown() {
|
|
129
|
+
stop()
|
|
129
130
|
writeState('review-loop', { status: 'stopped' }, STATE_DIR)
|
|
130
131
|
process.exit(0)
|
|
131
|
-
}
|
|
132
|
-
process.on('
|
|
133
|
-
|
|
134
|
-
writeState('review-loop', { status: 'stopped' }, STATE_DIR)
|
|
135
|
-
process.exit(0)
|
|
136
|
-
})
|
|
132
|
+
}
|
|
133
|
+
process.on('SIGINT', shutdown)
|
|
134
|
+
process.on('SIGTERM', shutdown)
|
|
137
135
|
|
|
138
136
|
writeState('review-loop', { status: 'running', startedAt: new Date().toISOString(), pid: process.pid }, STATE_DIR)
|
|
139
137
|
console.log('review-loop 시작. Ctrl+C로 종료.')
|