@elyun/bylane 1.29.0 → 1.31.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 +32 -8
- package/commands/bylane-cleanup.md +5 -0
- package/commands/bylane-code-agent.md +18 -0
- package/commands/bylane-issue-agent.md +46 -1
- package/commands/bylane-orchestrator.md +43 -1
- package/commands/bylane-setup.md +23 -5
- package/hooks/bylane-session-cleanup.js +94 -0
- package/package.json +1 -1
- package/src/cleanup.js +58 -8
- package/src/cli.js +22 -3
- package/src/config.js +4 -0
- package/src/pipeline.js +195 -0
- package/src/queue-utils.js +119 -0
- package/src/respond-loop.js +24 -2
- package/src/review-loop.js +24 -2
- package/tests/pipeline.test.js +171 -0
- package/tests/queue-utils.test.js +141 -0
- package/.bylane/bylane.json +0 -37
package/CLAUDE.md
CHANGED
|
@@ -11,7 +11,7 @@ byLane — Claude Code용 프론트엔드 개발 자동화 하네스.
|
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
13
|
npm install # 의존성 설치 + pre-commit 훅 자동 등록 (prepare 스크립트)
|
|
14
|
-
npm test # 테스트 실행 (
|
|
14
|
+
npm test # 테스트 실행 (43개)
|
|
15
15
|
npm run monitor # 모니터 대시보드
|
|
16
16
|
npm version minor # 마이너 버전 올리기 (커밋 + 태그 자동 생성)
|
|
17
17
|
npm run release # npm 배포 (커밋/푸시 완료 후 실행)
|
|
@@ -38,16 +38,19 @@ npx @elyun/bylane memory read 123
|
|
|
38
38
|
- `src/config.js` — `.bylane/bylane.json` 로드/저장/검증 (loadConfig, saveConfig, validateConfig, getAgentModel, DEFAULT_CONFIG)
|
|
39
39
|
- `src/branch.js` — 브랜치명 패턴 엔진 (buildBranchName, buildBranchNameFromConfig)
|
|
40
40
|
- `src/memory.js` — 이슈별 컨텍스트 메모리 유틸 (readIssueMemory, appendIssueMemory, listIssueMemories)
|
|
41
|
-
- `src/cli.js` — npx 설치 CLI (install, loop, --symlink 옵션, 기존 파일 .bak
|
|
41
|
+
- `src/cli.js` — npx 설치 CLI (install, loop, --symlink 옵션, 기존 파일 .bak 백업, Stop 훅 등록)
|
|
42
42
|
- `src/loop-utils.js` — 루프 공통 유틸 (killExistingLoop, tmux 세션 관리, createAbsoluteTimer, resolveLoopMode)
|
|
43
|
-
- `src/
|
|
44
|
-
- `src/
|
|
43
|
+
- `src/queue-utils.js` — 큐 상태 관리 유틸 (reconcileQueue, expireStaleItems, gcQueue, maintainQueue)
|
|
44
|
+
- `src/pipeline.js` — 파이프라인 상태 추적 (startPipeline, updatePipelineStep, cancelStalePipeline, blockDownstreamOfFailed)
|
|
45
|
+
- `src/review-loop.js` — review 요청 PR 폴러 → `.bylane/state/review-queue.json` (절대시간 기반 폴링, 큐 reconcile/GC 포함)
|
|
46
|
+
- `src/respond-loop.js` — 내 PR 리뷰/코멘트 폴러 → `.bylane/state/respond-queue.json` (절대시간 기반 폴링, 큐 reconcile/GC 포함)
|
|
45
47
|
- `src/monitor/` — blessed 기반 TUI 대시보드 (2열 그리드, 1초 폴링, fullUnicode)
|
|
46
48
|
- `skills/` — Claude Code 에이전트 skill 파일들
|
|
47
49
|
- `hooks/` — 외부 이벤트 자동 감지 훅
|
|
48
50
|
- `commands/` — `/bylane` 슬래시 커맨드 정의
|
|
49
51
|
- `templates/review-template.md` — 리뷰 코멘트 기본 템플릿
|
|
50
52
|
- `scripts/release.sh` — npm 배포 스크립트 (dirty 체크 + 테스트 + publish)
|
|
53
|
+
- `hooks/bylane-session-cleanup.js` — Stop 훅 (세션 종료 시 in_progress→cancelled, 루프/큐 상태 정리)
|
|
51
54
|
- `.githooks/pre-commit` — 보안 검사 훅 (시크릿/민감파일/console.log)
|
|
52
55
|
|
|
53
56
|
## 에이전트 파이프라인
|
|
@@ -87,7 +90,7 @@ respond-loop (독립: 설정 주기로 리뷰 코멘트 감지, 절대시간
|
|
|
87
90
|
```json
|
|
88
91
|
{
|
|
89
92
|
"agent": "code-agent",
|
|
90
|
-
"status": "in_progress | completed | failed | idle",
|
|
93
|
+
"status": "in_progress | completed | failed | cancelled | blocked | idle",
|
|
91
94
|
"startedAt": "ISO8601",
|
|
92
95
|
"progress": 0,
|
|
93
96
|
"retries": 0,
|
|
@@ -95,6 +98,25 @@ respond-loop (독립: 설정 주기로 리뷰 코멘트 감지, 절대시간
|
|
|
95
98
|
}
|
|
96
99
|
```
|
|
97
100
|
|
|
101
|
+
### 큐 항목 상태 값
|
|
102
|
+
|
|
103
|
+
`pending` → `reviewing`/`responding` → `resolved` | `expired` (GC 후 제거)
|
|
104
|
+
|
|
105
|
+
큐 reconcile: 매 poll마다 GitHub 상태와 대조하여 불필요한 pending 항목을 resolved로 전환.
|
|
106
|
+
TTL: 24시간 초과 pending → expired. GC: resolved/expired 후 1시간 경과 시 큐에서 제거.
|
|
107
|
+
|
|
108
|
+
### 파이프라인 상태 (`pipeline.json`)
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"agent": "pipeline",
|
|
113
|
+
"status": "in_progress | completed | failed | cancelled",
|
|
114
|
+
"pipelineType": "A | B | C_review | C_respond | D",
|
|
115
|
+
"currentStep": "code-agent",
|
|
116
|
+
"steps": [{ "agent": "code-agent", "status": "completed" }, ...]
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
98
120
|
## GitHub 접근 방법
|
|
99
121
|
|
|
100
122
|
`github.method`: `"auto"` (기본, MCP→CLI→API 순) | `"mcp"` | `"cli"` | `"api"`
|
|
@@ -114,9 +136,11 @@ respond-loop (독립: 설정 주기로 리뷰 코멘트 감지, 절대시간
|
|
|
114
136
|
|
|
115
137
|
```
|
|
116
138
|
tests/
|
|
117
|
-
├── state.test.js
|
|
118
|
-
├── config.test.js
|
|
119
|
-
|
|
139
|
+
├── state.test.js — writeState, readState, clearState, listStates, appendLog (6개)
|
|
140
|
+
├── config.test.js — loadConfig, saveConfig, validateConfig, DEFAULT_CONFIG (7개)
|
|
141
|
+
├── branch.test.js — buildBranchName (6개 패턴 케이스, 슬래시 엣지케이스 포함)
|
|
142
|
+
├── queue-utils.test.js — reconcileQueue, expireStaleItems, gcQueue, maintainQueue (10개)
|
|
143
|
+
└── pipeline.test.js — startPipeline, updatePipelineStep, cancelStalePipeline, blockDownstreamOfFailed (14개)
|
|
120
144
|
```
|
|
121
145
|
|
|
122
146
|
## 주의사항
|
|
@@ -25,11 +25,16 @@ npx @elyun/bylane cleanup
|
|
|
25
25
|
|
|
26
26
|
| 항목 | 동작 |
|
|
27
27
|
|------|------|
|
|
28
|
+
| stale 파이프라인 | 30분 초과 파이프라인 cascade cancel (하위 에이전트 일괄 취소) |
|
|
29
|
+
| upstream 실패 | 파이프라인 내 실패 에이전트 하류 → `blocked` 처리 |
|
|
28
30
|
| `.bylane/state/` 권한 | 디렉토리 755, 파일 644로 수정 |
|
|
29
31
|
| 죽은 루프 프로세스 | PID 확인 → 없으면 `stopped`로 전환 |
|
|
30
32
|
| 30분 초과 `in_progress` | `failed`로 초기화 |
|
|
31
33
|
| `subagents.json` active | PID 없는 항목 제거 |
|
|
32
34
|
| 큐의 `reviewing`/`responding` | `pending`으로 복구 (재처리 대기) |
|
|
35
|
+
| 큐 TTL 초과 pending | 24시간 초과 → `expired` 전환 |
|
|
36
|
+
| 큐 GC | resolved/expired 후 1시간 경과 항목 제거 |
|
|
37
|
+
| 루프-큐 상태 동기화 | 루프 미실행 시 큐 `stopped` 전환 |
|
|
33
38
|
|
|
34
39
|
## 완료
|
|
35
40
|
|
|
@@ -30,9 +30,27 @@ npx @elyun/bylane state write code-agent '{"status":"in_progress","startedAt":"'
|
|
|
30
30
|
| `spec.figmaSpec` | Figma 컬러토큰·컴포넌트 구조 |
|
|
31
31
|
| `issueType` | `new-feature` / `bug` / `improvement` / `chore` |
|
|
32
32
|
| `issueNumber` | GitHub 이슈 번호 |
|
|
33
|
+
| `branchName` | issue-agent가 생성한 브랜치명 |
|
|
34
|
+
| `worktreePath` | 워크트리 경로 (없으면 `null`) |
|
|
33
35
|
|
|
34
36
|
없으면 GitHub 이슈 본문을 직접 로드하여 파싱한다.
|
|
35
37
|
|
|
38
|
+
### 브랜치/워크트리 확인
|
|
39
|
+
|
|
40
|
+
issue-agent가 이미 브랜치를 생성했는지 확인한다:
|
|
41
|
+
|
|
42
|
+
1. `branchName`이 있으면 → 해당 브랜치로 checkout
|
|
43
|
+
2. `worktreePath`가 있으면 → 해당 워크트리 디렉토리에서 작업
|
|
44
|
+
3. 둘 다 없으면 (`issue.autoCreateBranch: false`) → code-agent가 직접 브랜치 생성
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# branchName이 있는 경우
|
|
48
|
+
git checkout "$BRANCH_NAME"
|
|
49
|
+
|
|
50
|
+
# worktreePath가 있는 경우 — 해당 디렉토리에서 모든 작업 수행
|
|
51
|
+
cd "$WORKTREE_PATH"
|
|
52
|
+
```
|
|
53
|
+
|
|
36
54
|
issueMemory 로드:
|
|
37
55
|
```bash
|
|
38
56
|
npx @elyun/bylane memory read ISSUE_NUMBER
|
|
@@ -211,6 +211,49 @@ curl -s -X POST \
|
|
|
211
211
|
|
|
212
212
|
---
|
|
213
213
|
|
|
214
|
+
### Phase 5 — 브랜치/워크트리 생성 (설정에 따라)
|
|
215
|
+
|
|
216
|
+
이슈 생성 직후, `issue.autoCreateBranch`가 `true`이면 브랜치를 자동 생성한다.
|
|
217
|
+
브랜치명은 `branch.pattern` 설정에 따라 결정된다.
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
# 브랜치명 생성 (src/branch.js 활용)
|
|
221
|
+
BRANCH_NAME=$(node -e "
|
|
222
|
+
import('./src/branch.js').then(({buildBranchNameFromConfig}) => {
|
|
223
|
+
import('./src/config.js').then(({loadConfig}) => {
|
|
224
|
+
const config = loadConfig()
|
|
225
|
+
const name = buildBranchNameFromConfig(config, {
|
|
226
|
+
issueNumber: ISSUE_NUMBER,
|
|
227
|
+
type: 'ISSUE_TYPE',
|
|
228
|
+
title: 'ISSUE_TITLE'
|
|
229
|
+
})
|
|
230
|
+
process.stdout.write(name)
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
")
|
|
234
|
+
|
|
235
|
+
git checkout -b "$BRANCH_NAME"
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
#### 워크트리 생성 (`issue.autoCreateWorktree: true`인 경우)
|
|
239
|
+
|
|
240
|
+
브랜치 대신 워크트리를 생성하여 독립된 작업 디렉토리를 만든다:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
WORKTREE_DIR=".bylane/worktrees/$BRANCH_NAME"
|
|
244
|
+
git worktree add "$WORKTREE_DIR" -b "$BRANCH_NAME"
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
이후 모든 에이전트(code-agent, test-agent, commit-agent 등)는 이 워크트리 경로에서 작업한다.
|
|
248
|
+
state에 `branchName`과 `worktreePath`를 기록하여 후속 에이전트가 참조할 수 있게 한다.
|
|
249
|
+
|
|
250
|
+
#### 설정이 꺼져 있는 경우
|
|
251
|
+
|
|
252
|
+
`issue.autoCreateBranch: false`이면 이 단계를 건너뛴다.
|
|
253
|
+
code-agent가 실행 시점에 브랜치를 직접 생성한다.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
214
257
|
## 출력
|
|
215
258
|
|
|
216
259
|
`.bylane/state/issue-agent.json`:
|
|
@@ -230,7 +273,9 @@ curl -s -X POST \
|
|
|
230
273
|
"affectedFiles": ["src/hooks/useTheme.ts", "src/components/Header/"],
|
|
231
274
|
"checklist": ["ThemeToggle 컴포넌트 생성", "useTheme hook 수정", "테스트 작성"],
|
|
232
275
|
"figmaSpec": { "enabled": false, "components": [], "colorTokens": {} }
|
|
233
|
-
}
|
|
276
|
+
},
|
|
277
|
+
"branchName": "issues-123-dark-mode-toggle",
|
|
278
|
+
"worktreePath": null
|
|
234
279
|
}
|
|
235
280
|
```
|
|
236
281
|
|
|
@@ -31,6 +31,41 @@ npx @elyun/bylane models
|
|
|
31
31
|
|
|
32
32
|
---
|
|
33
33
|
|
|
34
|
+
## 파이프라인 상태 추적
|
|
35
|
+
|
|
36
|
+
각 에이전트 호출 전후에 파이프라인 상태를 기록하여, 세션 종료나 에이전트 실패 시 자동 정리가 가능하게 한다.
|
|
37
|
+
|
|
38
|
+
### 파이프라인 시작 (에이전트 호출 전)
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx @elyun/bylane state write pipeline '{"status":"in_progress","pipelineType":"A","issueNumber":null,"startedAt":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","currentStep":"issue-agent","steps":[{"agent":"issue-agent","status":"pending"},{"agent":"code-agent","status":"pending"},{"agent":"test-agent","status":"pending"},{"agent":"commit-agent","status":"pending"},{"agent":"pr-agent","status":"pending"},{"agent":"review-agent","status":"pending"},{"agent":"notify-agent","status":"pending"}]}'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 각 에이전트 시작/완료 시
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# 에이전트 시작 시 — 해당 step을 in_progress로
|
|
48
|
+
npx @elyun/bylane state write pipeline '현재 pipeline state에서 해당 step.status를 "in_progress"로 변경'
|
|
49
|
+
|
|
50
|
+
# 에이전트 완료 시 — 해당 step을 completed로, currentStep을 다음 단계로
|
|
51
|
+
npx @elyun/bylane state write pipeline '현재 pipeline state에서 해당 step.status를 "completed"로 변경, currentStep 갱신'
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 파이프라인 완료/실패 시
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# 성공
|
|
58
|
+
npx @elyun/bylane state write pipeline '{"status":"completed","completedAt":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'",...}'
|
|
59
|
+
|
|
60
|
+
# 실패 (maxRetries 초과 등)
|
|
61
|
+
npx @elyun/bylane state write pipeline '{"status":"failed","failedAt":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","reason":"...",...}'
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
> **중요**: 파이프라인 상태가 30분간 업데이트되지 않으면 cleanup이 자동으로 cascade cancel한다.
|
|
65
|
+
> 세션 종료 시 Stop 훅이 in_progress 파이프라인과 하위 에이전트를 즉시 cancelled로 전환한다.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
34
69
|
## 의도 파싱 규칙
|
|
35
70
|
|
|
36
71
|
### 파이프라인 A — 새 기능 / 이슈 없는 구현 요청
|
|
@@ -94,9 +129,13 @@ npx @elyun/bylane models
|
|
|
94
129
|
|
|
95
130
|
### 각 에이전트 호출 방법
|
|
96
131
|
|
|
97
|
-
Agent 도구에 아래 형식으로
|
|
132
|
+
Agent 도구에 아래 형식으로 전달한다. **호출 전후로 파이프라인 상태를 갱신**한다:
|
|
98
133
|
|
|
99
134
|
```
|
|
135
|
+
# 1. 호출 전: 파이프라인 step을 in_progress로 갱신
|
|
136
|
+
npx @elyun/bylane state write pipeline '{...현재 state에서 해당 step.status → "in_progress"}'
|
|
137
|
+
|
|
138
|
+
# 2. Agent 도구 호출
|
|
100
139
|
subagent_type: "general-purpose"
|
|
101
140
|
model: {models 명령으로 확인한 해당 에이전트 모델}
|
|
102
141
|
prompt: |
|
|
@@ -109,6 +148,9 @@ prompt: |
|
|
|
109
148
|
|
|
110
149
|
스킬 완료 후 .bylane/state/{에이전트명}.json 에 결과가 기록된다.
|
|
111
150
|
결과 파일의 status, 핵심 출력 필드만 응답으로 돌려줘.
|
|
151
|
+
|
|
152
|
+
# 3. 호출 후: 파이프라인 step을 completed/failed로 갱신, currentStep을 다음 에이전트로
|
|
153
|
+
npx @elyun/bylane state write pipeline '{...현재 state에서 해당 step.status → "completed", currentStep → 다음}'
|
|
112
154
|
```
|
|
113
155
|
|
|
114
156
|
---
|
package/commands/bylane-setup.md
CHANGED
|
@@ -83,7 +83,25 @@ Linear API Key는 환경변수명(`LINEAR_API_KEY`)으로 저장. 실제 키값
|
|
|
83
83
|
|
|
84
84
|
`permissions.scope`에 저장.
|
|
85
85
|
|
|
86
|
-
## Step 5/
|
|
86
|
+
## Step 5/10 — 이슈 생성 시 브랜치/워크트리
|
|
87
|
+
|
|
88
|
+
> 이슈 생성 후 자동으로 브랜치를 만들까요? (y/n, 기본: y)
|
|
89
|
+
|
|
90
|
+
- `y` (기본) → `issue.autoCreateBranch = true` — 이슈 생성 직후 브랜치 네이밍 패턴에 따라 브랜치 자동 생성
|
|
91
|
+
- `n` → `issue.autoCreateBranch = false` — code-agent 실행 시 수동 생성
|
|
92
|
+
|
|
93
|
+
`y` 선택 시 추가 질문:
|
|
94
|
+
|
|
95
|
+
> 브랜치와 함께 git worktree도 생성할까요? (y/n, 기본: n)
|
|
96
|
+
> worktree를 사용하면 이슈별로 독립된 작업 디렉토리가 생성됩니다.
|
|
97
|
+
> 여러 이슈를 동시에 작업할 때 유용합니다.
|
|
98
|
+
|
|
99
|
+
- `y` → `issue.autoCreateWorktree = true` — `.bylane/worktrees/{branch-name}/`에 워크트리 생성
|
|
100
|
+
- `n` (기본) → `issue.autoCreateWorktree = false` — 브랜치만 생성하고 checkout
|
|
101
|
+
|
|
102
|
+
모든 후속 에이전트(code-agent, test-agent, commit-agent 등)는 이슈 번호 기준으로 해당 브랜치/워크트리에서 작업한다.
|
|
103
|
+
|
|
104
|
+
## Step 6/10 — 리뷰 자동 Approve
|
|
87
105
|
|
|
88
106
|
> AI 리뷰에서 지적사항이 없을 때 자동으로 Approve할까요? (y/n, 기본: n)
|
|
89
107
|
|
|
@@ -92,7 +110,7 @@ Linear API Key는 환경변수명(`LINEAR_API_KEY`)으로 저장. 실제 키값
|
|
|
92
110
|
|
|
93
111
|
대부분의 팀에서는 사람이 최종 Approve하는 것을 권장한다.
|
|
94
112
|
|
|
95
|
-
## Step
|
|
113
|
+
## Step 7/10 — Loop 실행 모드
|
|
96
114
|
|
|
97
115
|
> Loop 실행 모드를 선택하세요:
|
|
98
116
|
> 1. tmux — tmux 세션에서 백그라운드 실행 (터미널 종료 후에도 유지, 권장)
|
|
@@ -117,7 +135,7 @@ bylane loop stop # loop 종료
|
|
|
117
135
|
bylane loop status # 상태 확인
|
|
118
136
|
```
|
|
119
137
|
|
|
120
|
-
## Step
|
|
138
|
+
## Step 8/10 — 고급 설정
|
|
121
139
|
|
|
122
140
|
> 고급 설정을 변경하시겠습니까? (Enter = 기본값 사용)
|
|
123
141
|
|
|
@@ -126,7 +144,7 @@ bylane loop status # 상태 확인
|
|
|
126
144
|
- `loopTimeoutMinutes` (기본: 30): 루프 타임아웃 (분)
|
|
127
145
|
- Figma MCP 활성화? (y/n, 기본: n)
|
|
128
146
|
|
|
129
|
-
## Step
|
|
147
|
+
## Step 9/10 — 브랜치 네이밍
|
|
130
148
|
|
|
131
149
|
> 브랜치 네이밍 패턴을 선택하세요:
|
|
132
150
|
> 1. {tracker}-{issue-number} 예) issues-32
|
|
@@ -141,7 +159,7 @@ bylane loop status # 상태 확인
|
|
|
141
159
|
직접 입력 시 사용 가능한 토큰 목록 안내:
|
|
142
160
|
`{tracker}`, `{type}`, `{issue-number}`, `{custom-id}`, `{title-slug}`, `{date}`, `{username}`
|
|
143
161
|
|
|
144
|
-
## Step
|
|
162
|
+
## Step 10/10 — 에이전트 모델 설정
|
|
145
163
|
|
|
146
164
|
> 각 에이전트에 사용할 AI 모델을 설정하시겠습니까? (Enter = 기본값 사용)
|
|
147
165
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bylane-session-cleanup.js
|
|
4
|
+
* Stop 훅 — Claude Code 세션 종료 시 상태 파일 자동 정리
|
|
5
|
+
*
|
|
6
|
+
* 처리 항목:
|
|
7
|
+
* - in_progress 에이전트 → cancelled (세션 종료)
|
|
8
|
+
* - running 루프 (PID 죽은 경우) → stopped
|
|
9
|
+
* - running 큐 (루프 죽은 경우) → stopped
|
|
10
|
+
* - pipeline in_progress → cancelled
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync, readdirSync, writeFileSync, existsSync } from 'fs'
|
|
13
|
+
|
|
14
|
+
// stdin 읽기 (훅 프로토콜)
|
|
15
|
+
try { readFileSync('/dev/stdin', 'utf8') } catch {}
|
|
16
|
+
|
|
17
|
+
const STATE_DIR = '.bylane/state'
|
|
18
|
+
if (!existsSync(STATE_DIR)) process.exit(0)
|
|
19
|
+
|
|
20
|
+
const now = new Date().toISOString()
|
|
21
|
+
|
|
22
|
+
function isPidAlive(pid) {
|
|
23
|
+
try { process.kill(Number(pid), 0); return true } catch { return false }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readJson(path) {
|
|
27
|
+
try { return JSON.parse(readFileSync(path, 'utf8')) } catch { return null }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function writeJson(path, data) {
|
|
31
|
+
writeFileSync(path, JSON.stringify({ ...data, updatedAt: now }, null, 2))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const files = readdirSync(STATE_DIR).filter(f => f.endsWith('.json'))
|
|
36
|
+
const { join } = await import('path')
|
|
37
|
+
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
const name = file.replace('.json', '')
|
|
40
|
+
if (name === 'cancel' || name === 'subagents') continue
|
|
41
|
+
|
|
42
|
+
const path = join(STATE_DIR, file)
|
|
43
|
+
const state = readJson(path)
|
|
44
|
+
if (!state) continue
|
|
45
|
+
|
|
46
|
+
// 파이프라인: in_progress → cancelled + 하위 step도 cancelled
|
|
47
|
+
if (name === 'pipeline' && state.status === 'in_progress') {
|
|
48
|
+
writeJson(path, {
|
|
49
|
+
...state,
|
|
50
|
+
status: 'cancelled',
|
|
51
|
+
cancelledAt: now,
|
|
52
|
+
reason: 'session_ended',
|
|
53
|
+
steps: (state.steps ?? []).map(step =>
|
|
54
|
+
step.status === 'in_progress' || step.status === 'pending'
|
|
55
|
+
? { ...step, status: 'cancelled', cancelledAt: now }
|
|
56
|
+
: step
|
|
57
|
+
)
|
|
58
|
+
})
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// in_progress 에이전트 → cancelled
|
|
63
|
+
if (state.status === 'in_progress' && !name.endsWith('-loop') && !name.endsWith('-queue')) {
|
|
64
|
+
writeJson(path, {
|
|
65
|
+
...state,
|
|
66
|
+
status: 'cancelled',
|
|
67
|
+
cancelledAt: now,
|
|
68
|
+
reason: 'session_ended'
|
|
69
|
+
})
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// running 루프: PID 죽었으면 stopped
|
|
74
|
+
if (name.endsWith('-loop') && state.status === 'running' && state.pid) {
|
|
75
|
+
if (!isPidAlive(state.pid)) {
|
|
76
|
+
writeJson(path, { ...state, status: 'stopped', stoppedAt: now })
|
|
77
|
+
}
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// running 큐: 대응 루프가 죽었으면 stopped
|
|
82
|
+
if (name.endsWith('-queue') && state.status === 'running') {
|
|
83
|
+
const loopName = name.replace('-queue', '-loop')
|
|
84
|
+
const loopPath = join(STATE_DIR, `${loopName}.json`)
|
|
85
|
+
const loopState = readJson(loopPath)
|
|
86
|
+
const loopAlive = loopState?.status === 'running' && loopState?.pid && isPidAlive(loopState.pid)
|
|
87
|
+
if (!loopAlive) {
|
|
88
|
+
writeJson(path, { ...state, status: 'stopped' })
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch {}
|
|
93
|
+
|
|
94
|
+
process.exit(0)
|
package/package.json
CHANGED
package/src/cleanup.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import { readdirSync, chmodSync, existsSync, unlinkSync, readFileSync, writeFileSync, mkdirSync } from 'fs'
|
|
7
7
|
import { join } from 'path'
|
|
8
8
|
import { readState, writeState } from './state.js'
|
|
9
|
+
import { gcQueue, expireStaleItems } from './queue-utils.js'
|
|
10
|
+
import { cancelStalePipeline, blockDownstreamOfFailed } from './pipeline.js'
|
|
9
11
|
|
|
10
12
|
const STATE_DIR = '.bylane/state'
|
|
11
13
|
const STALE_MS = 30 * 60 * 1000 // 30분 이상 in_progress → 초기화
|
|
@@ -73,6 +75,18 @@ export function runCleanup() {
|
|
|
73
75
|
return result
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
// 0. 파이프라인 cascade cancel — stale 파이프라인 감지 시 하위 에이전트 일괄 취소
|
|
79
|
+
const stalePipeline = cancelStalePipeline()
|
|
80
|
+
if (stalePipeline.pipelineCancelled) {
|
|
81
|
+
result.reset.push(`pipeline: stale → cancelled (${stalePipeline.cancelled.length}개 에이전트 취소: ${stalePipeline.cancelled.join(', ')})`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 0-1. 파이프라인 내 실패 에이전트 하류 → blocked
|
|
85
|
+
const downstream = blockDownstreamOfFailed()
|
|
86
|
+
if (downstream.blocked.length > 0) {
|
|
87
|
+
result.reset.push(`pipeline: upstream 실패 → ${downstream.blocked.length}개 에이전트 blocked (${downstream.blocked.join(', ')})`)
|
|
88
|
+
}
|
|
89
|
+
|
|
76
90
|
// 1. 파일 권한 수정
|
|
77
91
|
result.fixed.push(...fixPermissions())
|
|
78
92
|
|
|
@@ -82,8 +96,8 @@ export function runCleanup() {
|
|
|
82
96
|
for (const file of files) {
|
|
83
97
|
const name = file.replace('.json', '')
|
|
84
98
|
|
|
85
|
-
// 특수 파일 건너뜀
|
|
86
|
-
if (name === 'cancel') continue
|
|
99
|
+
// 특수 파일 건너뜀 (pipeline은 pipeline.js에서 별도 처리)
|
|
100
|
+
if (name === 'cancel' || name === 'pipeline') continue
|
|
87
101
|
|
|
88
102
|
// subagents 별도 처리
|
|
89
103
|
if (name === 'subagents') {
|
|
@@ -115,17 +129,53 @@ export function runCleanup() {
|
|
|
115
129
|
}
|
|
116
130
|
}
|
|
117
131
|
|
|
118
|
-
// 큐
|
|
132
|
+
// 큐 파일 종합 정리
|
|
119
133
|
if ((name === 'review-queue' || name === 'respond-queue') && Array.isArray(state.queue)) {
|
|
120
|
-
|
|
134
|
+
let queue = state.queue
|
|
135
|
+
let changed = false
|
|
136
|
+
|
|
137
|
+
// 1) responding/reviewing 상태 → pending 복구
|
|
138
|
+
const recovered = queue.map(item =>
|
|
121
139
|
item.status === 'reviewing' || item.status === 'responding'
|
|
122
140
|
? { ...item, status: 'pending', recoveredAt: new Date().toISOString() }
|
|
123
141
|
: item
|
|
124
142
|
)
|
|
125
|
-
const
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
143
|
+
const recoveredCount = recovered.filter((item, i) => item.status !== queue[i].status).length
|
|
144
|
+
if (recoveredCount > 0) {
|
|
145
|
+
queue = recovered
|
|
146
|
+
changed = true
|
|
147
|
+
result.reset.push(`${name}: ${recoveredCount}개 진행중 항목 → pending 복구`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 2) stale pending → expired (TTL 초과)
|
|
151
|
+
const expired = expireStaleItems(queue)
|
|
152
|
+
if (expired.expiredCount > 0) {
|
|
153
|
+
queue = expired.queue
|
|
154
|
+
changed = true
|
|
155
|
+
result.reset.push(`${name}: ${expired.expiredCount}개 pending 항목 → expired (TTL 초과)`)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 3) resolved/expired 항목 GC (1시간 경과)
|
|
159
|
+
const gc = gcQueue(queue)
|
|
160
|
+
if (gc.removedCount > 0) {
|
|
161
|
+
queue = gc.queue
|
|
162
|
+
changed = true
|
|
163
|
+
result.cleared.push(`${name}: ${gc.removedCount}개 완료/만료 항목 GC 제거`)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 4) 루프-큐 상태 동기화: 루프가 stopped인데 큐가 running이면 stopped로
|
|
167
|
+
const loopName = name.replace('-queue', '-loop')
|
|
168
|
+
const loopState = readState(loopName, STATE_DIR)
|
|
169
|
+
const loopDead = !loopState || loopState.status !== 'running' ||
|
|
170
|
+
(loopState.pid && !isPidAlive(loopState.pid))
|
|
171
|
+
if (state.status === 'running' && loopDead) {
|
|
172
|
+
changed = true
|
|
173
|
+
result.reset.push(`${name}: 루프 미실행 → 큐 상태 stopped`)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (changed) {
|
|
177
|
+
const newStatus = (state.status === 'running' && loopDead) ? 'stopped' : state.status
|
|
178
|
+
writeState(name, { ...state, status: newStatus, queue }, STATE_DIR)
|
|
129
179
|
}
|
|
130
180
|
}
|
|
131
181
|
}
|
package/src/cli.js
CHANGED
|
@@ -49,11 +49,14 @@ function registerHooks() {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
const hookScript = join(CLAUDE_DIR, 'hooks', 'bylane-agent-tracker.js')
|
|
52
|
+
const cleanupHookScript = join(CLAUDE_DIR, 'hooks', 'bylane-session-cleanup.js')
|
|
52
53
|
settings.hooks = settings.hooks ?? {}
|
|
53
54
|
|
|
54
55
|
// 기존 bylane 훅 제거 후 재등록 (버전 업 시 경로 변경 대응)
|
|
55
56
|
const stripBylane = (arr) =>
|
|
56
|
-
(arr ?? []).filter(h => !h.hooks?.some(hh =>
|
|
57
|
+
(arr ?? []).filter(h => !h.hooks?.some(hh =>
|
|
58
|
+
hh.command?.includes('bylane-agent-tracker') || hh.command?.includes('bylane-session-cleanup')
|
|
59
|
+
))
|
|
57
60
|
|
|
58
61
|
settings.hooks.PreToolUse = [
|
|
59
62
|
...stripBylane(settings.hooks.PreToolUse),
|
|
@@ -63,9 +66,14 @@ function registerHooks() {
|
|
|
63
66
|
...stripBylane(settings.hooks.PostToolUse),
|
|
64
67
|
{ matcher: 'Agent', hooks: [{ type: 'command', command: `node ${hookScript} post` }] }
|
|
65
68
|
]
|
|
69
|
+
settings.hooks.Stop = [
|
|
70
|
+
...stripBylane(settings.hooks.Stop),
|
|
71
|
+
{ matcher: '', hooks: [{ type: 'command', command: `node ${cleanupHookScript}` }] }
|
|
72
|
+
]
|
|
66
73
|
|
|
67
74
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2))
|
|
68
|
-
console.log(' ~ Hook: bylane-agent-tracker 등록 (
|
|
75
|
+
console.log(' ~ Hook: bylane-agent-tracker 등록 (PreToolUse + PostToolUse)')
|
|
76
|
+
console.log(' ~ Hook: bylane-session-cleanup 등록 (Stop — 세션 종료 시 상태 정리)')
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
function preservedConfigs() {
|
|
@@ -158,10 +166,13 @@ function uninstall() {
|
|
|
158
166
|
try {
|
|
159
167
|
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'))
|
|
160
168
|
const stripBylane = (arr) =>
|
|
161
|
-
(arr ?? []).filter(h => !h.hooks?.some(hh =>
|
|
169
|
+
(arr ?? []).filter(h => !h.hooks?.some(hh =>
|
|
170
|
+
hh.command?.includes('bylane-agent-tracker') || hh.command?.includes('bylane-session-cleanup')
|
|
171
|
+
))
|
|
162
172
|
settings.hooks = settings.hooks ?? {}
|
|
163
173
|
settings.hooks.PreToolUse = stripBylane(settings.hooks.PreToolUse)
|
|
164
174
|
settings.hooks.PostToolUse = stripBylane(settings.hooks.PostToolUse)
|
|
175
|
+
settings.hooks.Stop = stripBylane(settings.hooks.Stop)
|
|
165
176
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2))
|
|
166
177
|
console.log(' - Hook: bylane-agent-tracker 제거')
|
|
167
178
|
} catch {}
|
|
@@ -267,6 +278,10 @@ if (command === 'install') {
|
|
|
267
278
|
const sessionName = config.loop?.sessionName ?? 'bylane-loops'
|
|
268
279
|
|
|
269
280
|
if (subCmd === 'start') {
|
|
281
|
+
// 시작 전 상태 정리
|
|
282
|
+
const { runCleanup } = await import('./cleanup.js')
|
|
283
|
+
runCleanup()
|
|
284
|
+
|
|
270
285
|
const mode = resolveLoopMode()
|
|
271
286
|
|
|
272
287
|
if (mode === 'tmux') {
|
|
@@ -330,6 +345,10 @@ if (command === 'install') {
|
|
|
330
345
|
}
|
|
331
346
|
}
|
|
332
347
|
|
|
348
|
+
// 종료 후 상태 정리 (큐 상태 동기화 포함)
|
|
349
|
+
const { runCleanup } = await import('./cleanup.js')
|
|
350
|
+
runCleanup()
|
|
351
|
+
|
|
333
352
|
if (!stopped) {
|
|
334
353
|
console.log(' 실행 중인 루프가 없습니다.')
|
|
335
354
|
}
|