@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 CHANGED
@@ -11,7 +11,7 @@ byLane — Claude Code용 프론트엔드 개발 자동화 하네스.
11
11
 
12
12
  ```bash
13
13
  npm install # 의존성 설치 + pre-commit 훅 자동 등록 (prepare 스크립트)
14
- npm test # 테스트 실행 (19개)
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/review-loop.js` — review 요청 PR 폴러 `.bylane/state/review-queue.json` (절대시간 기반 폴링)
44
- - `src/respond-loop.js` — PR 리뷰/코멘트 폴러 `.bylane/state/respond-queue.json` (절대시간 기반 폴링)
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 — writeState, readState, clearState, listStates, appendLog
118
- ├── config.test.js — loadConfig, saveConfig, validateConfig, DEFAULT_CONFIG
119
- └── branch.test.js — buildBranchName (6개 패턴 케이스, 슬래시 엣지케이스 포함)
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
  ---
@@ -83,7 +83,25 @@ Linear API Key는 환경변수명(`LINEAR_API_KEY`)으로 저장. 실제 키값
83
83
 
84
84
  `permissions.scope`에 저장.
85
85
 
86
- ## Step 5/9리뷰 자동 Approve
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 6/9 — Loop 실행 모드
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 7/9 — 고급 설정
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 8/9 — 브랜치 네이밍
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 9/9 — 에이전트 모델 설정
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elyun/bylane",
3
- "version": "1.29.0",
3
+ "version": "1.31.0",
4
4
  "description": "Frontend development harness for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
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
- // 큐 파일: responding/reviewing 상태가 남아있으면 pending으로 복구
132
+ // 큐 파일 종합 정리
119
133
  if ((name === 'review-queue' || name === 'respond-queue') && Array.isArray(state.queue)) {
120
- const fixed = state.queue.map(item =>
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 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 복구`)
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 => hh.command?.includes('bylane-agent-tracker')))
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 => hh.command?.includes('bylane-agent-tracker')))
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
  }
package/src/config.js CHANGED
@@ -40,6 +40,10 @@ export const DEFAULT_CONFIG = {
40
40
  owner: '',
41
41
  repo: ''
42
42
  },
43
+ issue: {
44
+ autoCreateBranch: true,
45
+ autoCreateWorktree: false
46
+ },
43
47
  memory: {
44
48
  enabled: true,
45
49
  dir: '.bylane/memory'