@elyun/bylane 1.16.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +262 -121
- package/commands/bylane-cleanup.md +36 -0
- package/commands/bylane-code-agent.md +3 -24
- package/commands/bylane-commit-agent.md +3 -19
- package/commands/bylane-issue-agent.md +1 -1
- package/commands/bylane-notify-agent.md +1 -1
- package/commands/bylane-orchestrator.md +4 -23
- package/commands/bylane-pr-agent.md +2 -2
- package/commands/bylane-respond-agent.md +22 -31
- package/commands/bylane-respond-loop.md +6 -18
- package/commands/bylane-review-agent.md +112 -120
- package/commands/bylane-review-loop.md +6 -17
- package/commands/bylane-test-agent.md +1 -1
- package/package.json +1 -1
- package/src/cleanup.js +152 -0
- package/src/cli.js +39 -0
- package/src/monitor/index.js +96 -1
- package/src/monitor/layout.js +1 -1
- package/src/respond-loop.js +1 -1
- package/src/review-loop.js +1 -1
- package/templates/review-template.md +17 -47
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,45 @@ function install() {
|
|
|
141
141
|
|
|
142
142
|
if (command === 'install') {
|
|
143
143
|
install()
|
|
144
|
+
} else if (command === 'models') {
|
|
145
|
+
// models → 에이전트별 모델 목록 출력 (KEY=VALUE 형식)
|
|
146
|
+
const { loadConfig, getAgentModel } = await import('./config.js')
|
|
147
|
+
const config = loadConfig()
|
|
148
|
+
const agents = ['orchestrator','issue-agent','code-agent','test-agent',
|
|
149
|
+
'commit-agent','pr-agent','review-agent','respond-agent','notify-agent','analyze-agent']
|
|
150
|
+
agents.forEach(a => console.log(`${a}=${getAgentModel(config, a)}`))
|
|
151
|
+
} else if (command === 'branch') {
|
|
152
|
+
// branch ISSUE_NUMBER → 브랜치명 출력
|
|
153
|
+
const issueNumber = Number(args[1])
|
|
154
|
+
if (!issueNumber) { console.error('사용법: bylane branch <issueNumber>'); process.exit(1) }
|
|
155
|
+
const { buildBranchNameFromConfig } = await import('./branch.js')
|
|
156
|
+
const { loadConfig } = await import('./config.js')
|
|
157
|
+
console.log(buildBranchNameFromConfig(loadConfig(), issueNumber))
|
|
158
|
+
} else if (command === 'state') {
|
|
159
|
+
// state write AGENT '{"status":"in_progress",...}'
|
|
160
|
+
// state append AGENT "메시지"
|
|
161
|
+
// state read AGENT
|
|
162
|
+
const subCmd = args[1]
|
|
163
|
+
const agentName = args[2]
|
|
164
|
+
const payload = args[3]
|
|
165
|
+
const { writeState, appendLog, readState } = await import('./state.js')
|
|
166
|
+
|
|
167
|
+
if (subCmd === 'write' && agentName && payload) {
|
|
168
|
+
writeState(agentName, JSON.parse(payload))
|
|
169
|
+
} else if (subCmd === 'append' && agentName && payload) {
|
|
170
|
+
appendLog(agentName, payload)
|
|
171
|
+
} else if (subCmd === 'read' && agentName) {
|
|
172
|
+
console.log(JSON.stringify(readState(agentName), null, 2))
|
|
173
|
+
} else {
|
|
174
|
+
console.error('사용법: bylane state <write|append|read> <agentName> [payload]')
|
|
175
|
+
process.exit(1)
|
|
176
|
+
}
|
|
177
|
+
} else if (command === 'cleanup') {
|
|
178
|
+
const { runCleanup, formatCleanupResult } = await import('./cleanup.js')
|
|
179
|
+
console.log('\n byLane 상태 정리 중...\n')
|
|
180
|
+
const result = runCleanup()
|
|
181
|
+
console.log(formatCleanupResult(result))
|
|
182
|
+
console.log('\n 완료.\n')
|
|
144
183
|
} else if (command === 'monitor') {
|
|
145
184
|
// 항상 현재 패키지의 모니터 실행 (버전 일치 보장)
|
|
146
185
|
const monitorPath = join(__dirname, 'monitor', 'index.js')
|
package/src/monitor/index.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import blessed from 'blessed'
|
|
2
3
|
import { createLayout } from './layout.js'
|
|
3
4
|
import { createPoller } from './poller.js'
|
|
4
5
|
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'fs'
|
|
5
6
|
import { join } from 'path'
|
|
7
|
+
import { runCleanup, formatCleanupResult } from '../cleanup.js'
|
|
6
8
|
|
|
7
9
|
const { screen, header, pipeline, log, queue, status, onCleanup } = createLayout()
|
|
8
10
|
const poller = createPoller()
|
|
9
11
|
|
|
10
12
|
const startTime = Date.now()
|
|
11
13
|
const STATE_DIR = '.bylane/state'
|
|
14
|
+
let lastStates = {}
|
|
12
15
|
const SUBAGENTS_FILE = join(STATE_DIR, 'subagents.json')
|
|
13
16
|
const CANCEL_FILE = join(STATE_DIR, 'cancel.json')
|
|
14
17
|
|
|
@@ -20,6 +23,7 @@ function readSubagents() {
|
|
|
20
23
|
onCleanup(() => poller.stop())
|
|
21
24
|
|
|
22
25
|
poller.onChange((states) => {
|
|
26
|
+
lastStates = states
|
|
23
27
|
const active = Object.values(states).find(s => s.status === 'in_progress')
|
|
24
28
|
const workflowTitle = active ? (active.currentTask ?? active.agent) : 'Idle'
|
|
25
29
|
|
|
@@ -34,8 +38,99 @@ poller.onChange((states) => {
|
|
|
34
38
|
status.update()
|
|
35
39
|
})
|
|
36
40
|
|
|
41
|
+
// 's' — 실행 중인 루프 선택 종료
|
|
42
|
+
screen.key('s', () => {
|
|
43
|
+
const runningLoops = Object.entries(lastStates)
|
|
44
|
+
.filter(([name, s]) => name.endsWith('-loop') && s?.status === 'running' && s?.pid)
|
|
45
|
+
.map(([name, s]) => ({ name, pid: s.pid }))
|
|
46
|
+
|
|
47
|
+
if (runningLoops.length === 0) {
|
|
48
|
+
header.update({
|
|
49
|
+
workflowTitle: '실행 중인 루프 없음',
|
|
50
|
+
time: new Date().toLocaleTimeString('ko-KR', { hour12: false }),
|
|
51
|
+
elapsed: `${Math.floor((Date.now() - startTime) / 1000)}s`
|
|
52
|
+
})
|
|
53
|
+
screen.render()
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// blessed list 오버레이
|
|
58
|
+
const list = blessed.list({
|
|
59
|
+
top: 'center',
|
|
60
|
+
left: 'center',
|
|
61
|
+
width: 40,
|
|
62
|
+
height: runningLoops.length + 4,
|
|
63
|
+
label: ' 종료할 루프 선택 (Enter/Esc) ',
|
|
64
|
+
tags: true,
|
|
65
|
+
border: { type: 'line' },
|
|
66
|
+
style: {
|
|
67
|
+
border: { fg: 'yellow' },
|
|
68
|
+
selected: { bg: 'blue', fg: 'white' },
|
|
69
|
+
item: { fg: 'white' }
|
|
70
|
+
},
|
|
71
|
+
keys: true,
|
|
72
|
+
items: runningLoops.map(l => ` ${l.name} (PID: ${l.pid})`)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
screen.append(list)
|
|
76
|
+
list.focus()
|
|
77
|
+
screen.render()
|
|
78
|
+
|
|
79
|
+
list.on('select', (item, idx) => {
|
|
80
|
+
const loop = runningLoops[idx]
|
|
81
|
+
try {
|
|
82
|
+
process.kill(loop.pid, 'SIGTERM')
|
|
83
|
+
header.update({
|
|
84
|
+
workflowTitle: `${loop.name} 종료 요청됨 (PID: ${loop.pid})`,
|
|
85
|
+
time: new Date().toLocaleTimeString('ko-KR', { hour12: false }),
|
|
86
|
+
elapsed: `${Math.floor((Date.now() - startTime) / 1000)}s`
|
|
87
|
+
})
|
|
88
|
+
} catch {
|
|
89
|
+
header.update({
|
|
90
|
+
workflowTitle: `${loop.name} 종료 실패 (이미 종료됨?)`,
|
|
91
|
+
time: new Date().toLocaleTimeString('ko-KR', { hour12: false }),
|
|
92
|
+
elapsed: `${Math.floor((Date.now() - startTime) / 1000)}s`
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
screen.remove(list)
|
|
96
|
+
screen.render()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
list.key(['escape', 'q'], () => {
|
|
100
|
+
screen.remove(list)
|
|
101
|
+
screen.render()
|
|
102
|
+
})
|
|
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
|
+
|
|
37
133
|
// 'c' — 하위 에이전트 취소 플래그 토글
|
|
38
|
-
const cancelLabel = () => existsSync(CANCEL_FILE) ? '[취소 활성]' : ''
|
|
39
134
|
screen.key('c', () => {
|
|
40
135
|
if (existsSync(CANCEL_FILE)) {
|
|
41
136
|
unlinkSync(CANCEL_FILE)
|
package/src/monitor/layout.js
CHANGED
|
@@ -62,7 +62,7 @@ export function createLayout() {
|
|
|
62
62
|
left: 0,
|
|
63
63
|
width: '100%',
|
|
64
64
|
height: 1,
|
|
65
|
-
content: ' [q]종료 [c]에이전트취소토글 [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)
|
package/src/respond-loop.js
CHANGED
|
@@ -186,5 +186,5 @@ process.on('SIGTERM', () => {
|
|
|
186
186
|
process.exit(0)
|
|
187
187
|
})
|
|
188
188
|
|
|
189
|
-
writeState('respond-loop', { status: 'running', startedAt: new Date().toISOString() }, STATE_DIR)
|
|
189
|
+
writeState('respond-loop', { status: 'running', startedAt: new Date().toISOString(), pid: process.pid }, STATE_DIR)
|
|
190
190
|
console.log('respond-loop 시작. Ctrl+C로 종료.')
|
package/src/review-loop.js
CHANGED
|
@@ -133,5 +133,5 @@ process.on('SIGTERM', () => {
|
|
|
133
133
|
process.exit(0)
|
|
134
134
|
})
|
|
135
135
|
|
|
136
|
-
writeState('review-loop', { status: 'running', startedAt: new Date().toISOString() }, STATE_DIR)
|
|
136
|
+
writeState('review-loop', { status: 'running', startedAt: new Date().toISOString(), pid: process.pid }, STATE_DIR)
|
|
137
137
|
console.log('review-loop 시작. Ctrl+C로 종료.')
|
|
@@ -1,86 +1,56 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 리뷰 템플릿
|
|
2
2
|
|
|
3
3
|
이 파일을 복사해 프로젝트별로 커스터마이즈하세요.
|
|
4
4
|
`.bylane/bylane.json`의 `review.templateFile`에 경로를 지정하면 적용됩니다.
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
## 코멘트 형식
|
|
8
|
+
## 인라인 코멘트 형식
|
|
9
9
|
|
|
10
|
-
각 리뷰 코멘트는
|
|
10
|
+
각 리뷰 코멘트는 **해당 코드 라인에 직접** 작성합니다.
|
|
11
11
|
|
|
12
12
|
```
|
|
13
|
-
{
|
|
13
|
+
{title}
|
|
14
14
|
|
|
15
15
|
{description}
|
|
16
16
|
|
|
17
|
-
{
|
|
18
|
-
|
|
19
|
-
{suggestion}
|
|
17
|
+
{suggestion_block}
|
|
20
18
|
```
|
|
21
19
|
|
|
22
20
|
### 필드 설명
|
|
23
21
|
|
|
24
|
-
- `{
|
|
25
|
-
- `{title}` — 문제 제목 (한 줄)
|
|
22
|
+
- `{title}` — 문제 제목 (한 줄, 간결하게)
|
|
26
23
|
- `{description}` — 문제 설명 및 이유
|
|
27
|
-
- `{
|
|
28
|
-
- `{suggestion}` — 수정 방법 또는 개선 예시 코드
|
|
29
|
-
|
|
30
|
-
---
|
|
24
|
+
- `{suggestion_block}` — GitHub suggestion 블록 (수정 제안 코드)
|
|
31
25
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
| 심각도 | 기준 | 처리 |
|
|
35
|
-
|--------|------|------|
|
|
36
|
-
| CRITICAL | 버그, 보안 취약점, 데이터 손실 가능성 | 즉시 수정 필요, PR 차단 |
|
|
37
|
-
| HIGH | 잘못된 로직, 성능 심각 저하 | 수정 강력 권장 |
|
|
38
|
-
| MEDIUM | 코드 품질, 가독성, 테스트 누락 | 개선 권장 |
|
|
39
|
-
| LOW | 네이밍, 스타일, 선택적 개선 | 참고용 |
|
|
40
|
-
|
|
41
|
-
---
|
|
26
|
+
### suggestion 블록 형식
|
|
42
27
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
```language
|
|
47
|
-
// 문제가 있는 코드
|
|
28
|
+
````
|
|
29
|
+
```suggestion
|
|
30
|
+
// 수정된 코드 (원본 라인을 그대로 대체)
|
|
48
31
|
```
|
|
32
|
+
````
|
|
49
33
|
|
|
50
|
-
|
|
51
|
-
```language
|
|
52
|
-
// 개선된 코드
|
|
53
|
-
```
|
|
34
|
+
GitHub에서 "Apply suggestion" 버튼으로 바로 적용 가능합니다.
|
|
54
35
|
|
|
55
36
|
---
|
|
56
37
|
|
|
57
|
-
## 전체
|
|
38
|
+
## 전체 요약 형식 (PR 전체 코멘트)
|
|
58
39
|
|
|
59
40
|
```
|
|
60
|
-
##
|
|
61
|
-
|
|
62
|
-
| 심각도 | 건수 |
|
|
63
|
-
|--------|------|
|
|
64
|
-
| CRITICAL | N |
|
|
65
|
-
| HIGH | N |
|
|
66
|
-
| MEDIUM | N |
|
|
67
|
-
| LOW | N |
|
|
41
|
+
## 리뷰 요약
|
|
68
42
|
|
|
69
43
|
### 주요 발견사항
|
|
70
44
|
- ...
|
|
71
45
|
|
|
72
46
|
### 종합 의견
|
|
73
47
|
...
|
|
74
|
-
|
|
75
|
-
---
|
|
76
|
-
{footer}
|
|
77
48
|
```
|
|
78
49
|
|
|
79
50
|
---
|
|
80
51
|
|
|
81
52
|
## 푸터
|
|
82
53
|
|
|
83
|
-
기본값: `
|
|
54
|
+
기본값: `{model} · {date}`
|
|
84
55
|
|
|
85
|
-
`{model}` — 실제
|
|
86
|
-
커스터마이즈 예시: `AI 리뷰 by byLane (claude-sonnet-4-6) · {date}`
|
|
56
|
+
`{model}`, `{date}` — 실제 모델명과 날짜로 자동 치환됩니다.
|