@elyun/bylane 1.17.0 → 1.19.0

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