@bottari/skills 0.1.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 +50 -0
- package/bin/cli.js +133 -0
- package/package.json +17 -0
- package/playbook.md +49 -0
- package/skills/boss/SKILL.md +170 -0
- package/skills/boss/scripts/classify-risk.js +60 -0
- package/skills/boss/scripts/scan-bandaids.js +76 -0
- package/skills/decide/SKILL.md +85 -0
- package/skills/distill-doc/SKILL.md +95 -0
- package/skills/e2e/SKILL.md +50 -0
- package/skills/e2e/references/desktop.md +40 -0
- package/skills/e2e/references/mobile.md +36 -0
- package/skills/e2e/references/web.md +26 -0
- package/skills/spec/SKILL.md +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# @bottari/skills
|
|
2
|
+
|
|
3
|
+
보따리 팀의 **휴대용 Claude Code 스킬 모음**. 어느 repo에서나 한 줄로 설치한다 — 스킬을 그 repo의 `.claude/skills/`로 복사하고, 항상 적용되는 개발 플레이북을 `CLAUDE.md` 상단에 `@`-ref로 배선한다.
|
|
4
|
+
|
|
5
|
+
## 설치
|
|
6
|
+
|
|
7
|
+
대상 repo 루트에서:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npx @bottari/skills # 모든 스킬
|
|
11
|
+
npx @bottari/skills boss # 특정 스킬만
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
**설치하면**: `.claude/playbook.md`(always-on base) + `.claude/skills/<스킬>/` 가 복사되고, `CLAUDE.md` 상단에 playbook `@`-ref 한 줄이 삽입된다(`CLAUDE.md` 없으면 생성, 있으면 나머지는 그대로 보존). 스킬은 clean mirror 라 재실행하면 그대로 재싱크. repo 가 같은 이름의 *자체* 스킬을 이미 갖고 있으면 **건드리지 않고 보존**한다(우리가 vendor 한 것만 재싱크).
|
|
15
|
+
|
|
16
|
+
복사·배선한 결과를 **커밋**하면, 팀원은 제품 repo만 클론해도 스킬이 딸려온다(vendor 방식).
|
|
17
|
+
업데이트는 `npx @bottari/skills@latest <skill>` 다시 돌리고 재커밋. idempotent라 여러 번 돌려도 안전.
|
|
18
|
+
|
|
19
|
+
## 사용법
|
|
20
|
+
|
|
21
|
+
스킬은 **`/이름`으로 직접 부르거나**(`/spec`, `/boss` …), description 의 *자연어 트리거*로 자동 발동한다(각 스킬 트리거는 아래). `playbook.md` 는 설치되면 *항상* 로드돼 코딩 내내 기본값이 되므로 따로 부를 필요 없다.
|
|
22
|
+
|
|
23
|
+
**한 task 의 흐름:**
|
|
24
|
+
|
|
25
|
+
1. **`/spec`** — 착수 전, 요구사항을 열린 질문 0 까지 인터뷰해 단단한 계획. *(여기서 시작)*
|
|
26
|
+
2. 그 계획대로 **개발**(playbook 원칙이 always-on 가이드) — 결정이 막히면 **`/decide`**.
|
|
27
|
+
3. **`/boss`** — 완료·품질을 clean PASS 까지.
|
|
28
|
+
4. **`/e2e`** — 실제 앱 구동 검증(필요 시).
|
|
29
|
+
5. **`/distill-doc`** — 끝난 계획을 decision 으로 증류.
|
|
30
|
+
|
|
31
|
+
## 수록 스킬
|
|
32
|
+
|
|
33
|
+
- **spec** (`/spec`) — 착수 *전*, 요구사항을 열린 질문 0 까지 인터뷰해 실제 시스템에 발 딛고(추측 X) 단단한 계획(spec)을 만든다 — garbage-in 방지. boss 의 대칭 *입력 게이트*.
|
|
34
|
+
*트리거*: "계획 짜자" · "스펙 떠줘" · "이거 어떻게 만들지" · "요구사항 정리" · "기획".
|
|
35
|
+
- **boss** (`/boss`) — task를 끝냈다 싶을 때, 정말 끝났고(plan 대비 누락·테스트·e2e) 정말 잘 짰는지(최선의 접근·임시 땜빵 없음·다음 개발 쉬운 구조)를 *맥락 없는 새 눈*으로 적대적으로 보고 clean PASS까지 가는 완료·품질 게이트(까다롭지만 네 편인 보스 톤). 어떤 스킬을 깔든 base로 함께 깔리는 `playbook.md`는 코딩 *중* always-on 기본값이자 boss의 리뷰 기준(rubric).
|
|
36
|
+
*트리거*: "보스" · "다 됐는지 봐줘" · "누락 없나" · "이게 최선이야?" · "머지/커밋 전 점검".
|
|
37
|
+
- **e2e** (`/e2e`) — 프로젝트 환경(web/mobile/desktop)을 감지해 그에 맞는 실제 앱 e2e를 돌린다(LLM이 화면 보고 판단·조작).
|
|
38
|
+
*트리거*: "e2e 돌려" · "앱 띄워 확인" · "화면 보고 검증" · "스크린샷".
|
|
39
|
+
- **distill-doc** (`/distill-doc`) — 실행 완료된 `docs/plans/` 계획 문서를 `docs/decisions/` 토픽 문서로 증류하고 plan 제거.
|
|
40
|
+
*트리거*: "plan 증류" · "decisions 정리" · "문서 정돈".
|
|
41
|
+
- **decide** (`/decide`) — 결정이 막혔을 때(LLM이 물어보는데 나도 모를 때) 웹서치로 여러 각도 조사·비교해 *근거 있는 최적 추천*을 준다.
|
|
42
|
+
*트리거*: "결정해줘" · "이거 뭐가 나아?" · "모르겠어 찾아줘" · "조언".
|
|
43
|
+
|
|
44
|
+
## 개발
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
node bin/cli.js --target /path/to/target-repo boss # publish 전 로컬 vendor
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
새 스킬 추가: `skills/<name>/SKILL.md`(+필요하면 `references/`)만 두면 CLI 가 자동 수록한다. 개발 플레이북은 레포 루트의 단일 `playbook.md` 한 파일 — 어떤 스킬을 깔든 base 로 항상 설치되고 `CLAUDE.md` 에 always-on `@`-ref 로 배선된다(`--no-playbook` 로 생략).
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// @bottari/skills — vendor portable Claude Code skills into a repo.
|
|
5
|
+
// Always installs playbook.md as an always-on base (.claude/playbook.md
|
|
6
|
+
// + @-ref in CLAUDE.md), then copies the requested skills into .claude/skills/.
|
|
7
|
+
// Idempotent: safe to re-run (re-vendor on update, then re-commit).
|
|
8
|
+
//
|
|
9
|
+
// Usage:
|
|
10
|
+
// npx @bottari/skills # playbook base + all skills
|
|
11
|
+
// npx @bottari/skills boss # playbook base + one skill
|
|
12
|
+
// npx @bottari/skills --target <repo> boss # explicit target (default: git root of cwd)
|
|
13
|
+
// npx @bottari/skills --no-playbook boss # skip the playbook base
|
|
14
|
+
//
|
|
15
|
+
// A same-named skill the repo already has (not vendored by us) is NEVER overwritten —
|
|
16
|
+
// it's kept and skipped. To replace it, delete it yourself first, then install.
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { execSync } = require('child_process');
|
|
21
|
+
|
|
22
|
+
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
23
|
+
const SKILLS_SRC = path.join(PKG_ROOT, 'skills');
|
|
24
|
+
const PLAYBOOK_SRC = path.join(PKG_ROOT, 'playbook.md');
|
|
25
|
+
const PLAYBOOK_REF = '@.claude/playbook.md';
|
|
26
|
+
|
|
27
|
+
function listSkills() {
|
|
28
|
+
if (!fs.existsSync(SKILLS_SRC)) return [];
|
|
29
|
+
return fs
|
|
30
|
+
.readdirSync(SKILLS_SRC)
|
|
31
|
+
.filter((d) => fs.statSync(path.join(SKILLS_SRC, d)).isDirectory());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseArgs(argv) {
|
|
35
|
+
let target = null;
|
|
36
|
+
let playbook = true;
|
|
37
|
+
const positional = [];
|
|
38
|
+
for (let i = 0; i < argv.length; i++) {
|
|
39
|
+
const a = argv[i];
|
|
40
|
+
if (a === '--target') target = argv[++i];
|
|
41
|
+
else if (a.startsWith('--target=')) target = a.slice('--target='.length);
|
|
42
|
+
else if (a === '--no-playbook') playbook = false;
|
|
43
|
+
else if (!a.startsWith('-')) positional.push(a);
|
|
44
|
+
}
|
|
45
|
+
return { target, playbook, positional };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function repoRoot(target) {
|
|
49
|
+
if (target) return path.resolve(target);
|
|
50
|
+
try {
|
|
51
|
+
return execSync('git rev-parse --show-toplevel', {
|
|
52
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
53
|
+
})
|
|
54
|
+
.toString()
|
|
55
|
+
.trim();
|
|
56
|
+
} catch {
|
|
57
|
+
return process.cwd();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Insert the playbook @-ref near the top of CLAUDE.md (after the first heading),
|
|
62
|
+
// idempotently. Creates CLAUDE.md if absent.
|
|
63
|
+
function wireRef(file) {
|
|
64
|
+
const line =
|
|
65
|
+
`- **개발 플레이북 (항상 적용)**: ${PLAYBOOK_REF} — AI 코딩 원칙·방법론(모든 세션 always-on). ` +
|
|
66
|
+
'리뷰 게이트 `/boss` 가 이 문서를 rubric 으로 쓴다.';
|
|
67
|
+
let text = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
68
|
+
if (text.includes(PLAYBOOK_REF)) {
|
|
69
|
+
console.log(' CLAUDE.md: already wired');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (!text) {
|
|
73
|
+
fs.writeFileSync(file, `# CLAUDE.md\n\n${line}\n`);
|
|
74
|
+
console.log(' CLAUDE.md: created with playbook ref');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const lines = text.split('\n');
|
|
78
|
+
const idx = lines.findIndex((l) => /^# /.test(l));
|
|
79
|
+
if (idx === -1) lines.push('', line);
|
|
80
|
+
else lines.splice(idx + 1, 0, '', line);
|
|
81
|
+
fs.writeFileSync(file, lines.join('\n'));
|
|
82
|
+
console.log(' CLAUDE.md: wired playbook ref near top');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function main() {
|
|
86
|
+
const available = listSkills();
|
|
87
|
+
if (available.length === 0) {
|
|
88
|
+
console.error('no skills bundled in this package');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
const { target, playbook, positional } = parseArgs(process.argv.slice(2));
|
|
92
|
+
const wanted = positional.length ? positional : available;
|
|
93
|
+
|
|
94
|
+
const root = repoRoot(target);
|
|
95
|
+
console.log(`target repo: ${root}`);
|
|
96
|
+
|
|
97
|
+
// 1) playbook base (always-on) — unless opted out
|
|
98
|
+
if (playbook && fs.existsSync(PLAYBOOK_SRC)) {
|
|
99
|
+
const dest = path.join(root, '.claude', 'playbook.md');
|
|
100
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
101
|
+
fs.copyFileSync(PLAYBOOK_SRC, dest);
|
|
102
|
+
console.log(' installed: .claude/playbook.md');
|
|
103
|
+
wireRef(path.join(root, 'CLAUDE.md'));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 2) skills
|
|
107
|
+
const destSkills = path.join(root, '.claude', 'skills');
|
|
108
|
+
fs.mkdirSync(destSkills, { recursive: true });
|
|
109
|
+
for (const name of wanted) {
|
|
110
|
+
if (!available.includes(name)) {
|
|
111
|
+
console.error(` skip: unknown skill '${name}' (have: ${available.join(', ')})`);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const dest = path.join(destSkills, name);
|
|
115
|
+
const marker = path.join(dest, '.bottari-skill');
|
|
116
|
+
// never overwrite a same-named skill the repo already has (e.g. its own e2e)
|
|
117
|
+
if (fs.existsSync(dest) && !fs.existsSync(marker)) {
|
|
118
|
+
console.error(
|
|
119
|
+
` skip: '${name}' already exists here and wasn't vendored by @bottari/skills — ` +
|
|
120
|
+
`keeping yours untouched. (delete it yourself first if you want this one.)`
|
|
121
|
+
);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
fs.rmSync(dest, { recursive: true, force: true }); // clean mirror
|
|
125
|
+
fs.cpSync(path.join(SKILLS_SRC, name), dest, { recursive: true });
|
|
126
|
+
fs.writeFileSync(marker, '@bottari/skills\n'); // mark as ours → safe to re-sync next time
|
|
127
|
+
console.log(` vendored: ${name} -> .claude/skills/${name}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log('done. review the changes and commit them in the target repo.');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bottari/skills",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Bottari team's portable Claude Code skills (spec, boss, decide, e2e, distill-doc). Vendors skills into a repo's .claude/skills and wires the always-on playbook (dev principles + loop) into CLAUDE.md.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"bottari-skills": "bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"skills/",
|
|
11
|
+
"playbook.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=16.7.0"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT"
|
|
17
|
+
}
|
package/playbook.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# 개발 플레이북 — AI 코딩 원칙 · 방법론
|
|
2
|
+
|
|
3
|
+
## 개발 루프 (한눈에)
|
|
4
|
+
|
|
5
|
+
한 task 가 도는 순서 — 각 단계가 *무엇을 보고* 하는지:
|
|
6
|
+
|
|
7
|
+
1. **요구 합의** (`spec` 스킬) — *인터뷰*로 요구의 열린 결말을 없애 단단한 계획을 만든다(합의가 수렴할 때까지). 합의된 spec 이 작업 기준(→ 아래 #1).
|
|
8
|
+
- *결정이 막히면 — LLM이 옵션을 내미는데 모르겠으면 — 추측 말고 조사·다각도 비교로 추천받는다(같이 깔리는 `decide` 스킬). **AskUserQuestion 으로 비자명한 결정을 물을 땐 `🤔 모르겠음 → decide로 조사` 선택지를 같이 줘, 막힌 사용자가 한 번에 부르게.***
|
|
9
|
+
2. **개발 + 검증** — spec 을 **아래 원칙(#1~13)대로** 구현하고, 끝내기 전 테스트(필요하면 `e2e` 스킬로 실제 구동까지)를 *직접 green* 으로 만든다(→ #2).
|
|
10
|
+
- *들어가기 전, 작업 공간을 스케일·병렬성으로 정한다 — 작고 가역이면 현재 브랜치 바로 / 독립 기능·큰 변경이면 별도 브랜치 / 병렬·충돌 위험이면 worktree 격리. repo git 정책을 따르고 멋대로 자동 브랜치하지 않는다.*
|
|
11
|
+
3. **독립 리뷰** — "다 됐다"를 스스로 믿지 말고 *맥락 없는 새 눈*으로 완료·품질을 본다 — clean 될 때까지(리뷰 게이트 `boss` 스킬).
|
|
12
|
+
4. **기록 정리** — 결정 기록을 최신화하고 stale 한 건 정리, 다 끝난 계획·임시 문서는 *증류*해 결론만 남기고 retire 한다(끝난 일이 '할 일'인 척 남지 않게 → #11). 계획 문서가 있었다면 증류 전용 `distill-doc` 스킬로.
|
|
13
|
+
5. **마무리 — 커밋/푸시** — 끝나도 *자동으로 커밋/푸시하지 않는다*. 사용자에게 *지금 커밋/푸시할지* 묻고, 요청이 있을 때만 한다.
|
|
14
|
+
|
|
15
|
+
> `spec`·`boss`·`decide`·`e2e`·`distill-doc` 은 이 패키지에 함께 들어있다(같이 깔린다). *repo별 경로·세부*는 그 repo 의 `CLAUDE.md` 가 채운다.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 계획 · 검증
|
|
20
|
+
|
|
21
|
+
1. **생성 전에 spec/plan.** 모호한 요구를 LLM은 *그럴듯하지만 틀린 가정*으로 메꿔서 구현한다 — 예전엔 코드를 직접 쓰며 그 모호함이 드러났지만 프롬프트는 안 그렇다. 먼저 무엇을 만들지 짧게 합의하라. (예외: *한 문장으로 설명되고 **동시에** 가역·저위험*인 변경이면 plan 생략 — 작은 변경에 절차를 강요하지 않는다. 단 한 문장이어도 auth·결제·데이터·파괴적이면 plan 필수.)
|
|
22
|
+
|
|
23
|
+
2. **돌릴 수 있는 check를 남기고, 통과 전엔 "끝났다"고 하지 마라.** pass/fail 신호가 없으면 "끝난 것처럼 보임"이 유일한 신호가 되고, 결국 사람이 매번 손으로 검증하는 루프가 된다 — 이게 LLM 개발의 1번 실패 모드다. check = 이 변경의 회귀를 잡아줄 *에이전트가 직접 돌릴 수 있는 신호*(기존 테스트든 새 테스트든). 코드 변경이면 최소한 build·lint가 통과해야 하고, doc·prose·단순 rename처럼 자동 신호가 무의미한 변경은 *어떻게 확인했는지 한 줄로 남기면* 된다. (린터를 흉내 내라는 말이 아니다.)
|
|
24
|
+
|
|
25
|
+
## 구조 · 단순함
|
|
26
|
+
|
|
27
|
+
3. **만들기 전에 재사용 — 모듈·유틸·컴포넌트. 새 외부 의존성은 최후의 수단.** 새로 쓰기 전에 `grep`으로 기존 모듈·유틸·패턴을 찾아 그걸 쓰거나 따른다(LLM은 있는 걸 안 찾고 매번 새 헬퍼를 만들어 코드를 파편화시킨다). 새 외부 dependency는 마지막 선택 — stdlib·이미 있는 dep을 먼저 보고, 추가할 땐 *실존·정말 필요·관리되는지* 확인한다(존재하지 않거나 typosquat한 패키지를 끌어오는 사고가 흔하다).
|
|
28
|
+
|
|
29
|
+
4. **개념마다 한 곳 (SSOT, single source of truth).** 같은 결정·규칙·로직은 한 군데 산다. 흩어지면 하나 고치려고 여러 파일을 만지는 사태(shotgun surgery)가 나고, 반대로 *전부 한 곳에 몰아넣으면* 따로 바뀔 것들이 묶여 god module이 된다. 모으는 기준은 "한 파일에 두느냐"가 아니라 **"같은 이유로 함께 바뀌느냐"**(cohesion) — 함께 바뀌는 것만 모으고, 따로 바뀌는 건 가른다. ("기존 패턴을 따른다"는 *스타일을 맞춘다*는 뜻이지 새 공유 추상을 뽑아내라는 뜻이 아니다.)
|
|
30
|
+
|
|
31
|
+
5. **YAGNI — 오늘 확정된 필요만 짓는다.** 생성이 공짜처럼 느껴져 "미리 만들어두자"는 충동이 커지지만, 모든 모듈은 누군가 이해하고 나중에 지워야 하는 비용이다. 판단 기준은 **"다음 변경이 쌀까?"**(구조가 받쳐주나)이지 "미래 케이스를 다 커버했나?"가 아니다. *확장성 = 바꾸기 쉬운 구조*(낮은 결합·명확한 경계·갈아끼울 자리를 열어둠)지, *안 쓸 기능·추상을 미리 빌드*하는 게 아니다. 공유 추상은 같은 게 3번째 나올 때 뽑는다(rule of three) — 2번째까진 복제를 허용한다.
|
|
32
|
+
|
|
33
|
+
6. **가장 작은, 동작하는 diff — 되돌릴 수 있게.** 사람 리뷰가 병목이고 LLM은 빠르지만 틀릴 수 있으니, 큰 한 방보다 *되돌리기 쉬움*이 낫다. 파괴적 변경보다 더하는(additive)·flag로 끄고 켤 수 있는 변경을 선호하고, 한 커밋으로 롤백되게 둔다. 이 "최소 diff" 원칙은 *이번 변경*을 지배한다 — **기존에 이미 흩어져 있던 것**을 정리하는 건 별개 작업이다: 이번 변경이 그 중복을 *악화*시킬 때만 즉시 통합하고, 아니면 발견 사항으로 남기되 조용히 작업 범위를 부풀리지 않는다.
|
|
34
|
+
|
|
35
|
+
7. **공유 계약·심볼을 바꾸거나 지우면, 그걸 쓰는 쪽까지.** 시그니처·공개 API·스키마를 바꾸면 `grep`으로 *실제로 그걸 쓰는 곳*을 찾아 같이 고치거나 하위 호환을 유지한다(LLM은 자기 맥락에 없는 호출부를 모른 채 깨뜨린다). 죽은 코드를 지울 땐 그걸 가리키는 doc·참조까지 같은 변경에서 제거한다.
|
|
36
|
+
|
|
37
|
+
## 품질 · 안전 · 기록
|
|
38
|
+
|
|
39
|
+
8. **증상을 덮지 말고 root cause를 고쳐라.** "통과시켜"라는 압력 아래 LLM은 에러를 삼키고 경고를 끄는 쪽으로 흐른다. 무엇이 *왜* 실패하는지 짚고 거기를 고쳐라 — check를 억지로 초록불 만들려고 에러를 삼키지 마라. "동작한다"는 건 root cause까지 해결됐다는 뜻이다.
|
|
40
|
+
|
|
41
|
+
9. **proceed vs ask — 그리고 high-stakes 게이트.** 가역적·저위험·요구가 분명하면 묻지 말고 진행한다. 모호하거나·파괴적이거나·작업 범위를 넘어서면 멈추고 묻는다. auth·결제·권한·데이터·마이그레이션은 high-stakes — 테스트와 리뷰를 반드시 거치고, **사람 승인 없이 머지하지 않는다.**
|
|
42
|
+
|
|
43
|
+
10. **secure-by-default.** parameterized query를 쓰고(문자열로 SQL을 잇지 말고), 입력을 검증하고, secret은 환경변수에 둔다. 자격증명·secret·개인정보(PII)를 프롬프트나 로그·에러 메시지에 흘리지 않는다. LLM은 학습 데이터의 취약한 패턴을 그대로 재생산하고, 비전문 운영자는 그걸 못 잡는다.
|
|
44
|
+
|
|
45
|
+
11. **why를 남겨라.** 비자명한 결정·버린 대안·제약을 주석이나 커밋에 적는다. 코드에는 *무엇*만 남고 *왜*는 세션이 끝나면 증발한다 — 다음 세션(사람이든 LLM이든)은 그 why가 없으면 같은 실수를 되풀이한다. 그리고 *무엇을·왜·엣지 상황에서 어떻게 동작하는지* 스스로 설명 못 하면 아직 끝난 게 아니다.
|
|
46
|
+
|
|
47
|
+
12. **추측하지 말고 확인한다 — 네 지식은 stale 할 수 있다.** 시스템이 어떻게 도는지·어떤 값·어떤 시그니처인지 불확실하면 *그럴듯한 추측으로 메우지 마라* — 관련 코드를 읽고, 필요하면 직접 돌려 확인하고, 필요하면 웹으로 사실을 확인한다. **특히 *시간에 민감한* 것(라이브러리 버전·API·deprecated 여부·best practice)은 training 지식이 낡았을 수 있으니 — `date` 로 오늘을 받아 확인하고 *현재* 기준을 웹으로 검증한다.** 확인 못 한 건 *확인 못 했다*고 말한다(없는 확신 금지). LLM이 가장 자주 사고 치는 지점 — 그럴듯한 거짓·낡은 지식이 조용히 흘러든다.
|
|
48
|
+
|
|
49
|
+
13. **'되는' 게 아니라 '최선'을 — 속도보다 완결성.** 일은 LLM이 하니 *걸리는 시간은 큰 제약이 아니다* — 빨리 대충 넘기지 말고 철저·완결하게(검증·조사를 건너뛰며 시간 아끼지 마라). 계획·구현 매 결정마다 *그냥 되는 방법인가, 최선인가*를 자문하고, 불확실하면 #12로 확인·웹 리서치해 best practice를 검증한다. (단 '최선'≠과설계 — 솔루션 *크기*는 #5 YAGNI·#6 최소 diff 와 균형. 완결성은 *이해·검증·접근 품질*이지 더 많이 *짓는* 게 아니다.)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: boss
|
|
3
|
+
description: >-
|
|
4
|
+
개발한 task/기능이 "다 됐다" 싶을 때, 정말 끝났고(원래 plan 대비 누락 없이·테스트·가능하면 e2e까지 실제로 돌려서) 정말 잘 짰는지
|
|
5
|
+
(최선의 접근인가·임시 땜빵/HACK 없나·다음 개발이 쉬운 구조인가) 보스처럼 fresh-eyes로 적대적 리뷰하고
|
|
6
|
+
clean PASS 까지 반복하는 완료·품질 게이트. "보스", "boss", "보스 불러줘", "이거 보스",
|
|
7
|
+
"정말 끝난 거 맞아?", "누락 없는지 봐줘", "이게 최선이야?", "땜빵 아니야?", "task 다 했는데 검토해줘",
|
|
8
|
+
"머지/ship/커밋 전에 점검", "다음 작업 이어가기 좋게 됐는지" 류 요청에 반드시 발동. 개발 세션 끝물·커밋/머지 직전·"다 됐다"
|
|
9
|
+
싶을 때 적극 사용. (diff 라인 버그 위주인 code-review 와 달리 "원래 의도 대비 뭐가 빠졌나·이게 최선인가"를 본다.)
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# boss
|
|
13
|
+
|
|
14
|
+
"다 됐다"는 말을 액면 그대로 믿지 않는, *까다롭지만 네 편인 보스*. **정말 끝났고(완료) + 정말 잘 짰는지(품질)** 를 *맥락 없는 새 눈*으로 빈틈없이 짚어, 깨끗해질 때까지 본다.
|
|
15
|
+
|
|
16
|
+
**읽는 것 (rubric):**
|
|
17
|
+
- repo의 **`.claude/playbook.md`** — 어디서나 통하는 개발 원칙·방법론(이 스킬 설치 시 base로 같이 깔린다).
|
|
18
|
+
- 이 repo의 **`CLAUDE.md`** 와 기존 코드 관례 — repo 특화 규칙(예: i18n 키 메커니즘·네이밍·glossary). 구체 규칙은 여기서 집행한다.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 원칙 — 왜 이렇게 도는가
|
|
23
|
+
|
|
24
|
+
- **fresh-eyes로 띄운다.** 코드를 쓴 그 세션은 *자기 결과를 옳다고 우기는* 경향이 있다(자기 맥락에 갇혀 hallucinate correctness). 그래서 리뷰는 **이 대화를 모르는 새 subagent**에게 넘긴다. 작성자의 *확신·변명*은 빼고, *요구 명세*만 준다.
|
|
25
|
+
- **꼼꼼히 짚되, 트집을 지어내진 않는다.** 페르소나는 *말투*다. 무엇이 must-fix 인지는 근거(evidence)로만 정한다 — 캐릭터 유지하려 없는 흠을 만들면 그게 바로 망치는 길이다. 깨끗하면 군말 없이 PASS 를 준다.
|
|
26
|
+
- **risk에 비례한다.** 사소한 변경을 풀세트로 볶으면 마찰만 는다. 변경의 위험도로 깊이를 정한다.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 실행 (run) — 순서대로
|
|
31
|
+
|
|
32
|
+
컨텍스트 패킷 → risk-tier → Phase 0(완료) → Phase 1(품질) → verify → loop. 발견은 모두 아래 **finding 계약** 형식으로 낸다.
|
|
33
|
+
|
|
34
|
+
### 1. 컨텍스트 패킷 모으기 (리뷰어에게 줄 것)
|
|
35
|
+
|
|
36
|
+
리뷰어는 이 대화를 모른다. 그래서 메인 세션(여기)이 다음을 모아 *사실*로 넘긴다:
|
|
37
|
+
|
|
38
|
+
1. **무엇을 요청받았나** — 원래 목표를 *중립적인 한 단락*으로. (왜 X를 골랐다는 *변호*가 아니라, 무엇을 만들기로 했나.) 모르면 사용자에게 한 줄 물어라: "원래 뭘 하려던 task였지?"
|
|
39
|
+
2. **변경 내용** — `git diff`(미커밋) 또는 이번 작업 브랜치의 diff, 바뀐 파일 목록.
|
|
40
|
+
3. **시도했다 버린 대안** (알면) — "Y는 Z 때문에 배제" 같은 *flat fact*로. → 리뷰어가 이미 정해진 걸 다시 들쑤셔 false 지적을 내는 걸 막는다.
|
|
41
|
+
|
|
42
|
+
함께: repo `.claude/playbook.md` + repo `CLAUDE.md`·관례.
|
|
43
|
+
|
|
44
|
+
### 2. risk-tier 산정
|
|
45
|
+
|
|
46
|
+
`scripts/classify-risk.js` 가 바뀐 *경로*로 **초기 tier**를 준다(결정적 — LLM 추론보다 확실):
|
|
47
|
+
|
|
48
|
+
- **high**: `auth`·`payment`/결제·`migrations`/스키마·권한·돈·개인정보 경로.
|
|
49
|
+
- **trivial**: doc·주석·copy·테스트·단순 rename.
|
|
50
|
+
- **medium**: 그 외.
|
|
51
|
+
|
|
52
|
+
경로는 *과대 분류*할 수 있다(예: `auth/` 파일의 주석만 수정). 그래서 **boss 가 실제 diff 를 보고 조정**한다 — 변경이 정말 가벼우면 *근거 한 줄과 함께 내릴 수도 있다*("auth 경로지만 주석만, 로직 무변경 → trivial 로 낮춤"). 단 **운영자가 '괜찮아'라고 *말해서* 내리진 않는다 — 하향 근거는 코드(diff) 자체다**(rebuttal 에 안 흔들리게). 운영자는 언제든 *올릴* 수 있다.
|
|
53
|
+
|
|
54
|
+
tier 가 깊이를 정한다: trivial = 가벼운 단일 패스 / medium = 단일 fresh 리뷰 / high = 렌즈 fan-out + 별도 verify.
|
|
55
|
+
|
|
56
|
+
### 3. Phase 0 · 완료 게이트 (품질 렌즈보다 *먼저*)
|
|
57
|
+
|
|
58
|
+
품질을 따지기 전에 *정말 끝났나*부터 본다. 안 끝났으면 깊은 품질 리뷰는 낭비다.
|
|
59
|
+
|
|
60
|
+
- **plan 대비 누락**: 컨텍스트 패킷의 요청 항목이 다 반영됐나? 빠진 항목을 나열한다.
|
|
61
|
+
- **테스트가 있고, green인가**: 개발 단계에서 이미 *직접 돌려* green 으로 만들어 두는 게 원칙(`playbook.md` #2). boss 는 그 최근 출력을 근거로 인정하되, **high-tier거나 미심쩍으면 다시 돌려 확인**한다(추정 금지). 새 분기/로직에 테스트가 없으면 그 자체가 finding.
|
|
62
|
+
- **e2e**: 변경이 건드린 사용자 흐름은 가능하면 e2e 까지(같은 원칙 — 개발 단계 최근 green 이면 근거로 인정, high-tier면 재실행). **재발명하지 말고 host repo의 e2e 수단/스킬에 위임**한다(예: 그 repo에 `.claude/skills/e2e/` 가 있으면 호출). 플랫폼별 차이(web/mobile/desktop)는 그 e2e 스킬이 처리한다.
|
|
63
|
+
- **build·lint green.**
|
|
64
|
+
- ⚠️ **scope/risk 비례**: doc·trivial 변경에 e2e 를 강요하지 마라. *일어날 수 없는* 케이스용 테스트를 요구하지 마라(over-engineering). 누락은 finding 으로 남긴다.
|
|
65
|
+
|
|
66
|
+
### 4. Phase 1 · 품질 렌즈
|
|
67
|
+
|
|
68
|
+
LLM이 *유일하게 잘 놓치는* 각도(spine)를 먼저 보고, 라인-레벨은 나중에. **style nitpick 금지**(들여쓰기·따옴표 취향 같은 건 보지 않는다 — 그건 linter 일이다).
|
|
69
|
+
|
|
70
|
+
**spine (먼저, 무게 둠):**
|
|
71
|
+
- **approach** — 시니어라면 이걸 어떻게 짰을까? 지금 접근이 *최선*인가 그냥 *되긴 되는* 건가. 비자명·불확실하면 **웹서치로 더 나은/표준 방법·known pitfall·deprecated 여부를 외부 근거로 확인**(training 기억에 의존 금지). ⚠️ trivial 엔 웹서치 금지(비용·지연), 웹 근거는 *출처 URL과 함께* finding 근거로.
|
|
72
|
+
- **completeness** — 뭐가 빠졌나: 엣지·에러 경로·동시성·바뀐 계약의 소비자·`TODO`/stub/half-wired.
|
|
73
|
+
- **no-band-aid** — 임시 땜빵이 *최종인 척* 남았나: `HACK`/`FIXME`·삼킨 에러·`any` 캐스팅·끈 테스트·magic 상수·복붙. "이게 최선이야, 땜빵이야?" (결정적 1차: 이 스킬의 `scripts/scan-bandaids.js` 로 마커 후보를 빠짐없이 표면화 — 각 건은 판단, 정당한 TODO도 있음. ⚠️ **scan 깨끗 ≠ 땜빵 없음**: 마커 없는 hack(주석 없는 하드코딩·우회)은 못 잡으니 *렌즈가 진짜 판정자*다.)
|
|
74
|
+
- **simplicity** — 가장 단순한 정답인가, over-engineering 아닌가.
|
|
75
|
+
- **continuity** — ① 다음 변경이 *싼* 구조인가(낮은 결합·갈아끼울 자리 — 단 안 올 미래를 미리 짓는 건 아님) ② 하드코딩, 특히 사용자 대면/로케일 텍스트가 외부화됐나(repo 메커니즘) ③ 다음 세션/사람이 이어가기 쉬운가.
|
|
76
|
+
|
|
77
|
+
**보조 (fresh read 가 표면화한 것에 한정 — code-review 가 하는 라인-레벨 diff 재탕 금지):**
|
|
78
|
+
correctness(spec 대비 로직·경계) · robustness(silent failure·삼킨 에러) · security · tests(버그를 일부러 심으면 잡히나·mock 이 정답을 그대로 돌려주는 tautology 아닌가) · dependency(새 dep 이 실존·정말 필요한가) · fit/structure(SSOT 양방향: 파편화 ↔ god-object) · maintainability/perf(실제 영향 순).
|
|
79
|
+
|
|
80
|
+
**high-tier 추가**: 마이그/스키마 — 되돌릴 수 있나·기존 데이터 깨나·구버전과 공존하나(expand-contract).
|
|
81
|
+
|
|
82
|
+
**렌즈 충돌 중재**: simplicity ↔ continuity 가 같은 곳을 반대로 가리키면 → "다음 변경이 쌀까? + *구체적으로 임박한* 변경이 명명됐나"로 단일 판정. 임박 변경이 없으면 **simplicity 기본 승**(YAGNI).
|
|
83
|
+
|
|
84
|
+
### 5. verify (high-tier 만)
|
|
85
|
+
|
|
86
|
+
high-severity finding 은 **두 번째 fresh subagent**가 인용 라인을 대조해 confirm/drop 한다. 이게 진짜 환각 방어선이다 — "근거 못 대면 drop"보다 강하다.
|
|
87
|
+
|
|
88
|
+
### 6. loop — clean 까지
|
|
89
|
+
|
|
90
|
+
한 방에 안 끝낸다.
|
|
91
|
+
|
|
92
|
+
1. `CHANGES REQUIRED` → finding 의 paste-back fix 를 건넨다(비개발자면 메인 세션이 대신 적용).
|
|
93
|
+
2. 고친 뒤 **Phase 0 → verify 를 fresh 로 다시** 돈다.
|
|
94
|
+
3. **evidence-backed must-fix 가 남는 동안만** 반복. 매 라운드 운영자에게 평이하게: "아직 X 빠졌다 — 에이전트에 이렇게 시켜라."
|
|
95
|
+
|
|
96
|
+
risk-tier 가 루프 깊이를 게이트: trivial = 1 패스, high = clean 까지.
|
|
97
|
+
|
|
98
|
+
⚠️ **종료 앵커 (반드시 — '계속 볶기'가 '없는 흠 지어내기'로 타락하는 걸 막는 장치):**
|
|
99
|
+
- `your-call`·확인 권장 항목은 PASS 를 막지 않는다.
|
|
100
|
+
- 새 evidence-backed must-fix 가 안 나오는 라운드면 **멈춘다.**
|
|
101
|
+
- 루프를 이어가려고 finding 을 지어내지 않는다.
|
|
102
|
+
- **plan 충족 + 테스트 green + must-fix 0 → PASS, 끝.**
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## finding 계약
|
|
107
|
+
|
|
108
|
+
발견 하나하나 이 형식을 채운다 — 빈 칸이 있으면 그 finding 은 버린다:
|
|
109
|
+
|
|
110
|
+
- **severity** (blocker / major / minor)
|
|
111
|
+
- **위치** `file:line`
|
|
112
|
+
- **why (평이하게)** — 비개발자도 알아듣게 *왜 문제인지*. (운영자가 직접 못 잡으니 why 가 핵심)
|
|
113
|
+
- **fix** — *작성 에이전트에 그대로 붙여넣을 지시*로. (예: "user 조회를 parameterized query 로 바꿔라" — 비개발자가 패치를 손으로 못 대니까)
|
|
114
|
+
- **disposition** — `must-fix`(고쳐야 PASS) / `your-call`(선택)
|
|
115
|
+
- **confidence** high/med/low
|
|
116
|
+
- **evidence** — *인용한 코드 경로* 또는 *실제 명령 출력*. 근거를 못 대면 **버린다**(환각 차단).
|
|
117
|
+
|
|
118
|
+
규칙:
|
|
119
|
+
- **low-confidence 는 "확인 권장" 버킷으로** — 메인 action list(must-fix)에 안 올린다.
|
|
120
|
+
- "관측했다"는 근거는 **실제로 돌려서** 만든다(추정으로 적지 마라).
|
|
121
|
+
- **"틀렸다"(must-fix)는 높은 bar·근거 필수, "이렇게 하면 더 낫다"(your-call)는 제안으로** 분리한다.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 출력 형식
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
[보스 한마디 — 페르소나]
|
|
129
|
+
|
|
130
|
+
판정: PASS | CHANGES REQUIRED (risk-tier: trivial|medium|high)
|
|
131
|
+
|
|
132
|
+
완료 게이트(Phase 0): plan 누락 / 테스트 / e2e / build·lint — 각 ✓ 또는 빠진 것
|
|
133
|
+
|
|
134
|
+
must-fix (고쳐야 보낸다):
|
|
135
|
+
- [severity] file:line — why(평이) — fix(붙여넣을 지시) — confidence — evidence
|
|
136
|
+
|
|
137
|
+
your-call (선택):
|
|
138
|
+
- ...
|
|
139
|
+
|
|
140
|
+
확인 권장 (low-confidence):
|
|
141
|
+
- ...
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
clean PASS 면 must-fix 0, 보스가 군말 없이 인정하고 끝낸다.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 페르소나 — 보스
|
|
149
|
+
|
|
150
|
+
보고 톤은 *대충 안 넘어가는 보스*로 — 까다롭지만 네 편이고, 가볍게:
|
|
151
|
+
|
|
152
|
+
- "다 됐어? 테스트는 돌려봤고?"
|
|
153
|
+
- "이 `any` 는 좀 걸리는데 — 의도한 거야?"
|
|
154
|
+
- "여기 임시로 막아둔 것 같은데, 이게 최선이야?"
|
|
155
|
+
- PASS 시: "좋아, 깔끔하네. 이대로 가자."
|
|
156
|
+
|
|
157
|
+
⚠️ **말투 ≠ 판정.** 페르소나는 전달 방식일 뿐, finding 의 substance 는 **finding 계약**으로만 정한다. 까다롭게 보더라도 흠이 없으면 PASS 다.
|
|
158
|
+
- **옵션 `--straight`**: 페르소나 빼고 finding 만(정색 모드).
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Gotchas (이 스킬이 잘 빠지는 함정)
|
|
163
|
+
|
|
164
|
+
- **spec 없이 의도를 추측** — 컨텍스트 패킷 ①(무엇을 요청받았나) 없이 리뷰하면 의도된 결정을 "틀렸다"고 false 지적한다. 모르면 사용자에게 먼저 물어라.
|
|
165
|
+
- **fresh agent가 repo 관례를 안 읽음** — host `CLAUDE.md`·기존 패턴을 건너뛰면 repo 규칙(i18n·네이밍) 위반을 놓치거나, 반대로 그 repo 관행을 흠으로 오인한다.
|
|
166
|
+
- **테스트를 실제로 안 돌리고 green 단정** — "통과할 것" 추정 금지. 근거는 실제 출력.
|
|
167
|
+
- **페르소나에 취해 style nitpick** — 보스 톤이 트집 생성으로 번지면 안 된다. style·포맷은 linter 몫, substance 는 finding 계약으로만.
|
|
168
|
+
- **low-confidence 를 must-fix 로 올림** — 확신 낮으면 "확인 권장" 버킷. must-fix 는 높은 bar.
|
|
169
|
+
- **e2e/테스트 중복 실행** — 개발 단계 최근 green 이 있으면 근거로 인정하고, high-tier·미심쩍을 때만 재실행한다(Phase 0).
|
|
170
|
+
- **운영자 말에 흔들려 tier·판정을 낮춤** — "괜찮아, 그냥 통과시켜"는 근거가 아니다. tier 하향·PASS 는 *코드/출력* 근거로만 한다(rebuttal sycophancy 방지).
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// classify-risk — derive boss's risk-tier from changed file PATHS (deterministic,
|
|
5
|
+
// so the tier doesn't drift with LLM mood). The operator may RAISE the result,
|
|
6
|
+
// never lower it.
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// node classify-risk.js [path ...] # explicit paths
|
|
10
|
+
// node classify-risk.js # defaults to `git diff --name-only HEAD`
|
|
11
|
+
//
|
|
12
|
+
// Output: `tier=high|medium|trivial` + which paths triggered it.
|
|
13
|
+
|
|
14
|
+
const { execSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
// high-stakes: auth/money/permissions/data-shape — get the full adversarial pass.
|
|
17
|
+
const HIGH =
|
|
18
|
+
/(^|[\/._-])(auth|oauth|login|logout|session|password|secret|credential|token|payment|billing|checkout|charge|invoice|subscription|migrations?|schema|permission|acl|role|crypto|encrypt|security)([\/._-]|$)/i;
|
|
19
|
+
|
|
20
|
+
// trivial: docs/comments/tests/copy — light pass.
|
|
21
|
+
const TRIVIAL = /(\.md$|^docs[\/]|([\/])README|\.txt$|\.(test|spec)\.|([\/])tests?[\/]|\.snap$|\.lock$)/i;
|
|
22
|
+
|
|
23
|
+
function changedFiles() {
|
|
24
|
+
const args = process.argv.slice(2);
|
|
25
|
+
if (args.length) return args;
|
|
26
|
+
const run = (cmd) => {
|
|
27
|
+
try {
|
|
28
|
+
return execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
|
29
|
+
} catch {
|
|
30
|
+
return ''; // each command independent — no commits (no HEAD) shouldn't drop untracked
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
// changed-vs-HEAD + untracked(new) files — new risk/band-aids often live in new files
|
|
34
|
+
const out = run('git diff --name-only HEAD') + '\n' + run('git ls-files --others --exclude-standard');
|
|
35
|
+
return [...new Set(out.split('\n').map((s) => s.trim()).filter(Boolean))];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const files = changedFiles();
|
|
39
|
+
if (!files.length) {
|
|
40
|
+
console.log('tier=medium (no changed files detected — defaulting; pass paths explicitly)');
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const high = files.filter((f) => HIGH.test(f));
|
|
45
|
+
const nonTrivial = files.filter((f) => !TRIVIAL.test(f));
|
|
46
|
+
|
|
47
|
+
let tier;
|
|
48
|
+
if (high.length) tier = 'high';
|
|
49
|
+
else if (nonTrivial.length === 0) tier = 'trivial';
|
|
50
|
+
else tier = 'medium';
|
|
51
|
+
|
|
52
|
+
console.log(`tier=${tier}`);
|
|
53
|
+
if (high.length) {
|
|
54
|
+
console.log(' high-stakes paths:');
|
|
55
|
+
for (const f of high) console.log(` - ${f}`);
|
|
56
|
+
}
|
|
57
|
+
console.log(
|
|
58
|
+
` (${files.length} changed file(s) — INITIAL tier from paths. boss may lower it only with diff evidence ` +
|
|
59
|
+
`(e.g. comment-only edit in a high path), never on operator say-so; operator may raise.)`
|
|
60
|
+
);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// scan-bandaids — deterministic first pass for boss's no-band-aid lens. Surfaces
|
|
5
|
+
// stopgap markers in changed files so none slip through. Each hit still needs
|
|
6
|
+
// judgment (a TODO can be legit) — this is a CANDIDATE list, not a verdict.
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// node scan-bandaids.js [path ...] # explicit paths
|
|
10
|
+
// node scan-bandaids.js # defaults to `git diff --name-only HEAD`
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const { execSync } = require('child_process');
|
|
14
|
+
|
|
15
|
+
const PATTERNS = [
|
|
16
|
+
['TODO/FIXME/HACK/XXX', /\b(TODO|FIXME|HACK|XXX)\b/],
|
|
17
|
+
['suppressed type/lint', /@ts-ignore|@ts-expect-error|eslint-disable|#\s*type:\s*ignore|#\s*noqa/],
|
|
18
|
+
['any-cast', /\bas any\b|:\s*any\b/],
|
|
19
|
+
['skipped/focused test', /\.(skip|only)\s*\(/],
|
|
20
|
+
['leftover debug', /\bconsole\.(log|debug)\b|\bdebugger\b|\bprint\(/],
|
|
21
|
+
['hard sleep / long timeout', /\bsleep\s*\(|setTimeout\([^,]*,\s*\d{4,}/],
|
|
22
|
+
['swallowed error', /catch\s*\([^)]*\)\s*\{\s*\}|except[^:]*:\s*pass/],
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function changedFiles() {
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
if (args.length) return args;
|
|
28
|
+
const run = (cmd) => {
|
|
29
|
+
try {
|
|
30
|
+
return execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
|
31
|
+
} catch {
|
|
32
|
+
return ''; // each command independent — no commits (no HEAD) shouldn't drop untracked
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
// changed-vs-HEAD + untracked(new) files — band-aids often live in new files
|
|
36
|
+
const out = run('git diff --name-only HEAD') + '\n' + run('git ls-files --others --exclude-standard');
|
|
37
|
+
return [...new Set(out.split('\n').map((s) => s.trim()).filter(Boolean))];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const files = changedFiles().filter((f) => {
|
|
41
|
+
try {
|
|
42
|
+
return fs.statSync(f).isFile();
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!files.length) {
|
|
49
|
+
console.log('no changed files to scan');
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let hits = 0;
|
|
54
|
+
for (const f of files) {
|
|
55
|
+
let lines;
|
|
56
|
+
try {
|
|
57
|
+
lines = fs.readFileSync(f, 'utf8').split('\n');
|
|
58
|
+
} catch {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
lines.forEach((line, i) => {
|
|
62
|
+
for (const [label, re] of PATTERNS) {
|
|
63
|
+
if (re.test(line)) {
|
|
64
|
+
console.log(`${f}:${i + 1} [${label}] ${line.trim().slice(0, 120)}`);
|
|
65
|
+
hits++;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log(
|
|
73
|
+
hits
|
|
74
|
+
? `\n${hits} band-aid candidate(s) — judge each (some are legit; flag only what should be done properly).`
|
|
75
|
+
: 'no band-aid markers found.'
|
|
76
|
+
);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: decide
|
|
3
|
+
description: >-
|
|
4
|
+
설계·구현 중 결정이 막혔을 때 — LLM이 골라 달라는데 나도 잘 모르거나, 두세 방법 중 뭐가 나은지, 어떤
|
|
5
|
+
라이브러리·패턴·접근이 최적인지 확신이 안 설 때 — 웹서치로 여러 각도를 조사·비교해 *근거 있는 단일 추천*을 준다.
|
|
6
|
+
"decide", "조언", "도와줘 결정", "모르겠어 찾아줘", "이거 뭐가 나아?", "뭘 골라야 해?", "최적이 뭐야?",
|
|
7
|
+
"판단해줘", "리서치해서 정해줘", "어떤 게 맞아?" 류 요청에 발동. 특히 AskUserQuestion 등으로 결정을 요구받았는데
|
|
8
|
+
답을 모를 때. (확신 있는 사소한 선택엔 발동하지 말 것 — 비자명하고 잘못 고르면 비싼 결정용. 두세 후보 중 하나를 골라 결정을 푸는 게 목적 — 종합 다출처 리서치 리포트는 범위 밖(별도 deep-research 류 스킬이 있으면 그쪽).)
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# decide — 막힌 결정을 근거로 풀어준다
|
|
12
|
+
|
|
13
|
+
LLM이 "둘 중 골라줘" 하는데 너도 모를 때가 있다. 이 스킬은 그 갈림길을 **웹서치로 여러 각도 조사하고, 이 프로젝트 제약에 대고 비교해, 근거 있는 *하나*를 추천**한다. 최종 결정권은 너에게 있다 — 강요가 아니라 *판단 재료를 갖춘 추천*이다.
|
|
14
|
+
|
|
15
|
+
**불리는 법:** AskUserQuestion 으로 옵션이 떠 있는데 못 고르겠으면 — 'Other(직접 입력)'에 "decide 로 조사해줘"라고 답하거나, 그 질문에 미리 들어있는 *'🤔 모르겠음 → decide'* 선택지를 고르면 발동한다. 조사 뒤엔 아래 **AskUserQuestion 모드**로 추천과 함께 다시 띄워 클릭으로 정하게 한다.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 언제 쓰나 (이게 decide 할 결정인가)
|
|
20
|
+
|
|
21
|
+
- *취향*이거나 *되돌리기 쉬운* 선택이면 그냥 하나 고르고 진행해라(과하게 조사하면 마찰만 는다 — `playbook.md`의 proceed-vs-ask·YAGNI).
|
|
22
|
+
- decide는 **비자명하고, 잘못 고르면 비싼**(되돌리기 어렵거나 오래 끌고 갈) 결정용: 아키텍처 갈림길, 라이브러리/도구 선택, 데이터 모델, 보안 방식 등.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 실행 (run) — 순서대로
|
|
27
|
+
|
|
28
|
+
### 1. 결정을 또렷하게 프레이밍
|
|
29
|
+
|
|
30
|
+
- *무엇을* 정해야 하나 한 문장으로.
|
|
31
|
+
- **제약**을 모은다: 이 프로젝트의 기존 스택·관례(repo를 본다), 요구사항, 성능/보안/기한 제약. 제약이 결론을 좌우한다.
|
|
32
|
+
- 빠진 제약이 결론을 가를 것 같으면 사용자에게 **1~2개만 짧게** 묻는다. (decide가 또 다른 막힌 질문이 되지 않게.)
|
|
33
|
+
|
|
34
|
+
### 2. 옵션 도출
|
|
35
|
+
|
|
36
|
+
후보를 2~4개로. *이 프로젝트에서 자연스러운 것*(기존 스택·이미 쓰는 dep)을 우선 포함하고, *업계 표준/관행*을 더한다. "직접 구현"도 후보면 명시.
|
|
37
|
+
|
|
38
|
+
### 3. 다각도 웹 조사 (핵심)
|
|
39
|
+
|
|
40
|
+
각 옵션을 **외부 근거**로 평가한다 — training 기억이 아니라 *현재* 웹(출처 URL 확보):
|
|
41
|
+
- 성숙도·유지보수 상태(최근 릴리스·이슈), 생태계·커뮤니티
|
|
42
|
+
- **known pitfalls**·안티패턴·흔한 함정
|
|
43
|
+
- deprecated/EOL 여부, 보안 이슈(CVE)
|
|
44
|
+
- 성능·번들 영향(해당되면)
|
|
45
|
+
- **이 프로젝트 제약과의 적합성** (1번에서 모은 것)
|
|
46
|
+
|
|
47
|
+
규모 비례: 중대한 결정이면 옵션별로 병렬 조사(subagent fan-out)해도 좋다. 사소하면 가볍게.
|
|
48
|
+
|
|
49
|
+
### 4. 비교·판정
|
|
50
|
+
|
|
51
|
+
- 제약에 대고 trade-off를 표로 정리.
|
|
52
|
+
- **단일 추천 + 왜** — 그리고 *2순위와 뭐가 갈랐나*.
|
|
53
|
+
- **confidence**(high/med/low). 근거가 약하면 낮추고 그렇다고 말한다(없는 확신을 지어내지 않는다).
|
|
54
|
+
- **"언제 결론이 바뀌나"** — 어떤 제약/사실이 달라지면 다른 선택이 맞는지(미래의 너를 위한 안전장치).
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 출력 형식
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
결정: <무엇을 정하는가>
|
|
62
|
+
|
|
63
|
+
추천: <옵션> — <한 줄 이유> (confidence: high|med|low)
|
|
64
|
+
|
|
65
|
+
왜 이게 이겼나: <2순위와의 핵심 차이>
|
|
66
|
+
|
|
67
|
+
trade-off:
|
|
68
|
+
- <옵션 A>: 장 / 단 / 근거(URL)
|
|
69
|
+
- <옵션 B>: 장 / 단 / 근거(URL)
|
|
70
|
+
|
|
71
|
+
언제 다시 생각: <이 사실/제약이 바뀌면 재고>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**고르기만 하면 되게 (AskUserQuestion 모드):** 원래 결정이 옵션 선택형이거나 사용자가 클릭으로 정하길 원하면 — 위 내용을 *먼저 텍스트로* 보여 근거·출처·confidence 를 남긴 뒤(AskUserQuestion 은 맥락이 잘 유실되니 배경을 앞에 깔고), **AskUserQuestion** 으로 옵션을 다시 띄운다: **추천을 맨 앞 + 라벨에 "(추천)"**, 각 옵션의 한 줄 trade-off 는 description 에, (선택) 옵션별 깊은 비교는 preview 에. → 사용자는 클릭 한 번으로 결정.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Gotchas (이 스킬이 잘 빠지는 함정)
|
|
79
|
+
|
|
80
|
+
- **근거 없으면 지어내지 마라** — 못 찾았으면 confidence 를 낮추고 "확인 못 함"이라 밝힌다.
|
|
81
|
+
- **오래된·SEO 스팸 출처를 권위로 인용** — 발행일·릴리스 최신성을 확인하고 1차 출처(공식 문서·릴리스 노트)를 우선한다.
|
|
82
|
+
- **후보를 2개로 좁혀 진짜 최선을 놓침** — "직접 구현"·업계 표준 관행도 후보에 넣었나 한 번 더 본다.
|
|
83
|
+
- **프로젝트 제약 안 읽고 일반론** — 이 repo 의 스택·관례에 대고 판정한다(웹 일반론 ≠ 이 repo 최선).
|
|
84
|
+
- **over-research** — 사소·가역 결정은 조사 없이 바로 추천(과한 조사는 마찰).
|
|
85
|
+
- **추천이지 강요 아님** — 최종 결정은 사용자. 한쪽으로 몰지 말고 trade-off 를 솔직히 보인다.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: distill-doc
|
|
3
|
+
description: 완료된 plan 문서를 decision 문서로 distill/정돈할 때. 사용자가 "완료된 plan distill", "문서 정돈", "decisions 정리", "plan→decision", "decisions staleness 감사" 류로 요청하거나, 실행이 끝난 docs/plans/ 의 plan 을 docs/decisions/ 토픽 문서로 증류하고 plan 을 제거할 때 발동. (일반 문서 편집·리라이트가 아니라 완료 plan→decision 증류 + plan 삭제 전용.) plan distill / docs tidy / decision distillation / staleness audit.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# distill-doc — 완료된 plan 을 decision 문서로 증류
|
|
7
|
+
|
|
8
|
+
`docs/plans/` 의 plan 이 **실행 완료**되면, 그 plan 은 더 이상 "앞으로 할 일"이 아니다 — 무엇을
|
|
9
|
+
왜 그렇게 정했는지의 *기록*이 남았을 뿐이다. 이 스킬은 그 기록을 `docs/decisions/` 토픽 문서로
|
|
10
|
+
**증류(distill)** 하고 plan 파일을 제거하는, 이 레포 컨벤션 기반의 정돈 워크플로다.
|
|
11
|
+
|
|
12
|
+
> **왜 증류인가 (mirror 가 아니라).** plan 은 착수 전 탐색·측정·갈래까지 다 담은 긴 서사다.
|
|
13
|
+
> 그걸 통째로 decisions/ 로 옮기면 *결정*이 서사에 묻힌다. decision 문서는 **확정된 결론 +
|
|
14
|
+
> 그 근거만** 담는다 — blow-by-blow 과정(무엇을 시도했고 어떻게 측정했나)은 **git 히스토리**에
|
|
15
|
+
> 이미 있으니 버린다. "왜 이렇게 생겼나"를 묻는 사람이 5분에 읽는 문서가 목표.
|
|
16
|
+
|
|
17
|
+
## 컨벤션 (왜 이렇게)
|
|
18
|
+
|
|
19
|
+
- **`docs/plans/` = 미래의 일만.** 컨벤션: 착수 → 구현 → 완료되면 plans/ 에서 **삭제**
|
|
20
|
+
(완료 기록은 git 히스토리에). 완료된 plan 이 plans/ 에 남아 있으면 "할 일"과 "한 일"이
|
|
21
|
+
섞여 폴더가 거짓말을 한다.
|
|
22
|
+
- **`docs/decisions/` = 확정된 설계 기록.** 시스템 전역 결정 = 상위 아키텍처 문서(있으면)의
|
|
23
|
+
결정 시리즈, 주제별 깊은 결정 = 토픽 문서(예: `db-schema.md`·`api-design.md`…).
|
|
24
|
+
- **doc 의 일 = 코드의 *현재* 기술** (CLAUDE.md). 삭제된 걸 "완성"이라 광고하거나 코드와
|
|
25
|
+
어긋난 채 남은 문서는 없느니만 못하다.
|
|
26
|
+
|
|
27
|
+
## 실행 — 증류 (순서대로)
|
|
28
|
+
|
|
29
|
+
### 1. 대상 식별
|
|
30
|
+
실행이 **완료된** plan 을 고른다 — 남은 future-work 이 backlog 등 다른 곳에 이미 추적되고
|
|
31
|
+
있어, plan 자체엔 *과거의 결정*만 남은 상태여야 한다. 아직 미구현 항목이 plan 본문에 있으면
|
|
32
|
+
증류 대상이 아니다(그건 미래의 일 = plans/ 에 그대로 둔다).
|
|
33
|
+
|
|
34
|
+
### 2. decisions/ 토픽 문서 생성/확장
|
|
35
|
+
주제가 기존 토픽 문서에 속하면 그 문서에 섹션을 더하고, 새 주제면 새 파일을 만든다. 스타일은
|
|
36
|
+
기존 decision 문서가 있으면 그것과 **동일**하게, 없으면 아래 형식대로:
|
|
37
|
+
|
|
38
|
+
- **제목**: `# <주제> — 결정 근거 (why, not mirror)`
|
|
39
|
+
- **blockquote 머리말**: 코드 SSOT 포인터 — *실제 구현 파일 경로*(DDL·스키마·파서 등 결정이
|
|
40
|
+
물리적으로 사는 곳)를 가리키고, **"여기 손으로 미러링 안 함 — 드리프트 방지(궁금하면 그
|
|
41
|
+
파일을 봐라)"** 를 명시. 상위 맥락(architecture D-번호·관련 문서) 링크도 여기.
|
|
42
|
+
- **본문 = `## N. 주제 — ✅ 확정` 섹션들**: **확정된 결정 + 그 근거만**. "왜 A 가 아니라 B"
|
|
43
|
+
같은 결정의 *이유*는 남기되, 시도/측정 과정의 서사는 한 줄로 줄이거나 버린다(git 에 있음).
|
|
44
|
+
컬럼 목록·노드 kind 목록처럼 코드가 SSOT 인 건 손으로 베끼지 말 것.
|
|
45
|
+
|
|
46
|
+
### 3. 인바운드 링크 재지정
|
|
47
|
+
이 plan 으로 들어오는 **모든** 링크를 새 decision 문서로 갱신한다.
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
rg -l '<plan-파일명>' docs/ # 이 plan 을 가리키는 파일 전부
|
|
51
|
+
# → 각 참조를 새 decision 문서 경로로 수정
|
|
52
|
+
rg '<plan-파일명>' docs/ # 마지막 검증: 끊긴 참조 0
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
마지막 `rg` 가 **아무것도 출력하지 않아야** 한다 — 하나라도 남으면 죽은 링크다.
|
|
56
|
+
|
|
57
|
+
### 4. 잔여 future-work → backlog / 이슈 트래커
|
|
58
|
+
plan 에 남은 "앞으로 할 일"이 있으면 repo 의 backlog(예: `docs/plans/backlog.md`)나 이슈
|
|
59
|
+
트래커에 **한 줄 포인터**로 옮긴다 — 내용을 통째로 복사하지 말고 새 decision 문서를 가리키게
|
|
60
|
+
한다(상세는 거기서 읽도록). 중복은 드리프트의 씨앗이다.
|
|
61
|
+
|
|
62
|
+
### 5. plan 제거
|
|
63
|
+
```
|
|
64
|
+
git rm docs/plans/<plan>.md
|
|
65
|
+
```
|
|
66
|
+
서사·측정·갈래는 git 히스토리에 남으니 파일을 지우는 게 손실이 아니다. (커밋은 사용자가
|
|
67
|
+
명시적으로 요청할 때만 — CLAUDE.md.)
|
|
68
|
+
|
|
69
|
+
### 6. 보고
|
|
70
|
+
- 만든/확장한 decision 문서 경로 + 어떤 섹션을 추가했나
|
|
71
|
+
- 재지정한 인바운드 링크 목록 + `rg` 끊긴 참조 0 검증 결과
|
|
72
|
+
- backlog 로 옮긴 잔여 future-work(있으면)
|
|
73
|
+
- 제거한 plan 파일
|
|
74
|
+
|
|
75
|
+
## 별도 모드 — decisions staleness 감사 (읽기 전용)
|
|
76
|
+
|
|
77
|
+
"decisions 정리"·"staleness 감사" 요청이 *증류*가 아니라 *검증*이면 이 모드다. **편집하지
|
|
78
|
+
않는다 — 드리프트만 리포트한다.**
|
|
79
|
+
|
|
80
|
+
`docs/decisions/*.md` 의 각 주장(특히 blockquote 의 SSOT 파일 경로·`## N … ✅ 확정` 의
|
|
81
|
+
구체 결정)을 **실제 코드와 대조**한다:
|
|
82
|
+
|
|
83
|
+
- blockquote 가 가리키는 SSOT 파일이 아직 그 경로에 존재하나 (`rg`/파일 확인)
|
|
84
|
+
- "✅ 확정"이라 적힌 결정이 코드의 현재와 일치하나, 아니면 이미 바뀌었나
|
|
85
|
+
- 문서가 언급한 심볼·테이블·옵션이 코드에 아직 있나
|
|
86
|
+
|
|
87
|
+
산출 = **드리프트 항목 리스트만** (문서 N의 주장 X ↔ 코드 현재 Y, 위치). 고치는 건 사용자
|
|
88
|
+
승인 후 별도로 — 이 모드는 진단까지다.
|
|
89
|
+
|
|
90
|
+
## Gotchas (이 스킬이 잘 빠지는 함정)
|
|
91
|
+
|
|
92
|
+
- **미구현 항목 남은 plan 을 증류 대상으로 착각** — plan 본문에 아직 안 한 일이 있으면 그건 미래의 일(plans/ 에 둔다). *과거 결정만* 남았을 때 증류한다.
|
|
93
|
+
- **코드가 SSOT 인 걸 손으로 베껴 드리프트** — 컬럼·enum·노드 종류 같은 목록은 decision 문서에 복사하지 말고 실제 파일을 가리킨다.
|
|
94
|
+
- **인바운드 링크 갱신 누락 → 죽은 링크** — `rg` 최종 검증이 0 이어야 한다(§3).
|
|
95
|
+
- **지우기 전 미커밋** — 서사는 git 히스토리에 있으니 *커밋된 뒤* 지운다(커밋은 사용자 요청 시).
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: e2e
|
|
3
|
+
description: >-
|
|
4
|
+
실제 앱을 띄워 화면을 보고 검증하는 e2e 루프 — 단위 테스트가 아니라 *진짜 구동* 검증. 프로젝트 환경
|
|
5
|
+
(web / mobile / desktop)을 자동 감지해 그에 맞는 e2e 도구·절차로 돌린다. "e2e", "e2e 돌려",
|
|
6
|
+
"앱 띄워서 확인", "화면 보고 검증", "스크린샷 찍어봐", "실제로 동작하는지 봐줘" 류 요청, 또는 UI/흐름을
|
|
7
|
+
바꾼 뒤 실제 동작 확인이 필요할 때 발동. 이미 repo에 e2e 셋업(playwright/cypress/detox/maestro 등)이
|
|
8
|
+
있으면 재발명하지 말고 그걸 쓴다.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# e2e — 환경 인지형 실제 앱 검증
|
|
12
|
+
|
|
13
|
+
빌드된 *진짜 앱*을 띄워, 화면을 보면서(스크린샷/관측) 검증하는 루프. 핵심은 **환경을 먼저 감지**하고 거기 맞는 도구로 도는 것 — web·mobile·desktop은 e2e 방식이 다르다.
|
|
14
|
+
|
|
15
|
+
> 이 스킬은 검증으로 *안내하는 라우터*다 — 플랫폼을 감지해 맞는 절차로 연결한다. repo 에 이미 검증 드라이버·셋업이 있으면 그게 더 견고하니 그걸 쓴다(아래 1번). 진짜 견고성은 각 repo 가 자기 플랫폼 e2e 를 단단히 할 때 나온다.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 실행 (run) — 순서대로
|
|
20
|
+
|
|
21
|
+
### 1. 환경 감지 (먼저)
|
|
22
|
+
|
|
23
|
+
프로젝트 신호로 플랫폼을 분류한다:
|
|
24
|
+
|
|
25
|
+
- **이미 있는 e2e 셋업이 최우선 신호.** `playwright.config.*`, `cypress.config.*`, `.detoxrc*`, `e2e/` 디렉터리, `*.maestro.yaml`/`.maestro/`, `package.json`의 `e2e`/`test:e2e` 스크립트 → **그걸 그대로 쓴다(재발명 금지).**
|
|
26
|
+
- 없으면 의존성/구조로 추론:
|
|
27
|
+
- `electron` in deps → **desktop** → `references/desktop.md`
|
|
28
|
+
- `react-native`·`expo`·`ios/`+`android/`·`pubspec.yaml`(Flutter) → **mobile** → `references/mobile.md`
|
|
29
|
+
- `next`·`vite`·`react-dom`·`vue`·`svelte`(+electron/RN 아님) → **web** → `references/web.md`
|
|
30
|
+
- 애매하면 사용자에게 한 줄로 확인("이거 web/mobile/desktop 중 뭐로 띄우면 돼?").
|
|
31
|
+
|
|
32
|
+
감지한 플랫폼의 reference 파일을 읽고 그 절차를 따른다.
|
|
33
|
+
|
|
34
|
+
### 2. 공통 루프 (플랫폼 불문)
|
|
35
|
+
|
|
36
|
+
1. **build** — 검증 대상이 빌드돼 있는지(아니면 빌드). 추정 금지.
|
|
37
|
+
2. **launch** — 실제 앱/페이지를 띄운다.
|
|
38
|
+
3. **drive** — 검증할 사용자 흐름을 실제로 조작(클릭·입력·내비).
|
|
39
|
+
4. **observe** — 스크린샷/DOM/로그로 *눈으로* 확인. "통과한 것 같다"가 아니라 관측된 사실로.
|
|
40
|
+
5. **assert** — 기대 동작과 대조. 실패는 스크린샷·메시지와 함께 구체적으로.
|
|
41
|
+
6. **report** — 무엇을 어떤 화면으로 확인했는지. green/red 명확히.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Gotchas (이 스킬이 잘 빠지는 함정)
|
|
46
|
+
|
|
47
|
+
- **재발명 금지** — repo의 기존 e2e 도구·설정·스크립트가 있으면 그걸 쓴다.
|
|
48
|
+
- **실제로 돌려라** — 돌리지 않고 "될 것"이라 적지 마라(관측 ≠ 추정).
|
|
49
|
+
- **scope 비례** — doc·순수 로직 변경엔 e2e 강요 금지. 바뀐 *사용자 흐름*만 본다.
|
|
50
|
+
- **mobile/web 절차는 *출발점*** — 플랫폼별 구체 절차는 reference 파일에 있고, 그 repo 셋업에 맞춰 단단히 해야 한다(검증된 건 desktop/Electron 경로).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# desktop e2e (Electron)
|
|
2
|
+
|
|
3
|
+
Electron 앱은 **Playwright 의 `_electron`**으로 실제 프로세스를 띄워 검증한다 — 가장 검증된 경로.
|
|
4
|
+
|
|
5
|
+
*LLM 이 화면을 보고 판단하며 조작하는 인터랙티브 루프*다(미리 짠 스크립트가 아니라 — 한 단계 조작 → `screenshot` → 판단 → 다음). Playwright 의 `getByRole`/`getByText` 로케이터 = *의미 기반 조작*(좌표 아님), `screenshot` = LLM 이 화면을 봄(mobile 의 agent-device snapshot+ref 와 같은 역할).
|
|
6
|
+
|
|
7
|
+
> **repo 에 자체 e2e 스킬·셋업이 있으면 그걸 우선한다.** 이 문서는 *일반 Electron* fallback 이다 — 성숙한 repo 는 자기 하네스(빌드·세션·DB seed·셀렉터 규약)가 더 정확하니 거기 따른다.
|
|
8
|
+
|
|
9
|
+
## 루프
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
코드 수정 → (빌드 포함) e2e 실행 → 스크린샷 Read → 화면 보고 진단 → 반복
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**스크린샷이 곧 눈이다.** 실패해도 마지막 스크린샷은 남는다 — 반드시 Read 로 열어 *뭐가 떠 있는지* 확인한 뒤 진단(추정 금지). main 프로세스 콘솔을 테스트 출력으로 중계(`[main]`/`[main:err]` 프리픽스)하면 로그도 화면과 같이 본다.
|
|
16
|
+
|
|
17
|
+
## 치트시트 (Playwright × Electron)
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { _electron as electron } from 'playwright';
|
|
21
|
+
const app = await electron.launch({ args: ['.'], cwd: appDir }); // 빌드 산출물 디렉터리
|
|
22
|
+
const win = await app.firstWindow(); // 첫 BrowserWindow = 일반 Page
|
|
23
|
+
await win.screenshot({ path: 'shots/x.png' }); // 본다
|
|
24
|
+
await win.getByRole('button', { name: 'Ask', exact: true }).click(); // 조작 (user-centric 셀렉터)
|
|
25
|
+
await win.getByPlaceholder('...').fill('hello'); await win.keyboard.press('Enter');
|
|
26
|
+
await expect(win.getByText('...')).toBeVisible({ timeout: 15_000 });
|
|
27
|
+
await app.close(); // afterAll 에서 반드시
|
|
28
|
+
```
|
|
29
|
+
- 셀렉터는 role·텍스트·placeholder 우선, CSS/XPath 는 최후.
|
|
30
|
+
- **main 프로세스 접근**: `app.evaluate(({ app }) => app.getPath('userData'))` — 단 evaluate 안은 *샌드박스*(require/import 불가), 외부 값은 인자로 직렬화해 넘긴다.
|
|
31
|
+
- 디버깅: `playwright test --debug` / `--trace on` / `-g "제목"`.
|
|
32
|
+
|
|
33
|
+
## Gotchas (실측 — 일반 Electron+Playwright)
|
|
34
|
+
|
|
35
|
+
- **single-instance 락** — dev 앱(또는 다른 인스턴스)이 떠 있으면 e2e 새 인스턴스가 조용히 죽고 `launch` 가 "Target page has been closed" 로 실패. `ps aux | grep -i electron` 으로 확인·종료 후 재실행(바이너리 이름은 보통 앱명이 아니라 `Electron`). config 는 `workers: 1`.
|
|
36
|
+
- **`--use-mock-keychain` 함정** — Playwright 의 electron loader 가 이 Chromium 스위치를 주입 → `safeStorage` 가 mock 키를 써서 *OS 키체인에 저장된 토큰 복호화가 조용히 실패* → 멀쩡한 세션인데 로그인 화면. 키체인/safeStorage 쓰는 앱이면 main 첫머리에서 `app.commandLine.removeSwitch('use-mock-keychain')`(평소엔 no-op).
|
|
37
|
+
- **빌드 필수** — e2e 는 dev 서버가 아니라 *빌드 산출물*을 `loadFile` 로 띄운다. 코드 고치고 빌드 없이 재실행하면 옛 화면. 빌드 포함 스크립트를 기본으로.
|
|
38
|
+
- **외부 인증**(OAuth 등) 자동화 안 함 — 사람이 1회 로그인 → 세션을 userData/키체인에서 *복원*해 재사용. 세션 없으면 skip 말고 *명확히 실패*(사용자에 로그인 요청).
|
|
39
|
+
- **결정성** — 외부 호출(LLM·네트워크) 의존 화면은 seed/격리 DB·샘플 데이터로 재현 가능하게. 로케일·테마가 화면을 바꾸면 환경변수로 고정.
|
|
40
|
+
- **toolchain** — CI/격리 환경에선 playwright 바이너리가 프로젝트에 provision 돼 있어야(전역 가정 금지).
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# mobile e2e
|
|
2
|
+
|
|
3
|
+
모바일은 **LLM 이 화면을 보고 판단하며 직접 조작**하는 식으로 검증한다 — 미리 짠 플로우가 아니라 *snapshot → 판단 → 액션 → 재확인* 인터랙티브 루프(데스크톱의 Playwright 스크린샷 루프와 같은 결).
|
|
4
|
+
|
|
5
|
+
## 도구: agent-device (권장)
|
|
6
|
+
|
|
7
|
+
[agent-device](https://github.com/callstackincubator/agent-device) — 시뮬레이터/에뮬레이터를 LLM 이 조종하는 OSS CLI. 시뮬/에뮬을 *raw 좌표 탭*으로 다루지 않고 **UI 트리 + ref + 스크린샷**을 줘서, LLM 이 화면을 보고 ref 로 조작한다. iOS(simctl)·Android(adb) 둘 다.
|
|
8
|
+
|
|
9
|
+
- 설치: `npm install -g agent-device@latest` (Node 22+)
|
|
10
|
+
- 사전: 앱이 시뮬/에뮬에 설치돼 있어야 한다(`flutter run -d <sim>` 한 번, 또는 `xcrun simctl install booted <app>` / `adb install`).
|
|
11
|
+
|
|
12
|
+
### 루프 (open → snapshot → action → verify → close)
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
agent-device open <appId> --platform ios # launch (android: --platform android)
|
|
16
|
+
agent-device snapshot -i # UI 트리 + ref(@e1, @e2 …)
|
|
17
|
+
agent-device click @eN # 또는: agent-device find "시작하기" click
|
|
18
|
+
agent-device screenshot out.png --overlay-refs # 화면 + ref 박스 → LLM 이 눈으로 확인
|
|
19
|
+
agent-device snapshot -i # 액션 후 상태 재확인
|
|
20
|
+
agent-device close
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- **ref(`@e1`…)는 snapshot 마다 재할당** — snapshot 결과 들고 한참 뒤 click 금지. *snapshot → 즉시 액션* 패턴.
|
|
24
|
+
- 라벨/한글 매칭 `find "시작하기" click` · 위젯 속성 `get attrs @eN` · 알림 `push` · 로그 `logs start/path`. (`agent-device help workflow` 가 가장 정확.)
|
|
25
|
+
- **Flutter 호환** ✅ — Flutter 위젯이 native accessibility tree 로 노출(text/button/image role·한글 라벨 정확). RN·네이티브도 동일.
|
|
26
|
+
|
|
27
|
+
## Gotchas (실증)
|
|
28
|
+
|
|
29
|
+
- **시뮬 상태 복원** — `close`+`open` 해도 fresh 아님(마지막 화면 복원). 특정 화면 검증은 ① 화면-비종속 assertion(권장: 어떤 화면에서도 참인 universal 검증) ② 재설치(`simctl uninstall` + `install`, 느림) ③ 로그인된 fixture 시뮬 중 택1.
|
|
30
|
+
- **외부 인증**(Google/Apple Sign-In) 자동화 어려움 → 이미 로그인된 시뮬 인스턴스를 fixture 로.
|
|
31
|
+
- **시계/timezone/DST** — `simctl` 로 시뮬 시계·tz 직접 변경 명령이 없다(호스트 변경은 위험). *시각 정밀* 검증은 e2e 말고 **unit test**(TZ env 바꿔 케이스 cover)가 적합.
|
|
32
|
+
- **실기기 전용** — DST 전환 순간 race 등은 시뮬로 재현 불가 → 실기기 체크리스트로.
|
|
33
|
+
|
|
34
|
+
## 스크립트 회귀 (별개 용도)
|
|
35
|
+
|
|
36
|
+
*LLM 없이* CI 에서 결정적으로 돌리는 회귀가 필요하면 Maestro(YAML)·Detox(RN)·Flutter `integration_test` 가 있다. 단 이건 *미리 짠 플로우*라, 이 스킬의 "LLM 이 보고 판단·조작" 모델과는 목적이 다르다(둘 다 둘 수 있음: 인터랙티브 검증=agent-device, 회귀 게이트=스크립트).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# web e2e
|
|
2
|
+
|
|
3
|
+
웹앱은 **Playwright**(또는 repo가 이미 쓰면 Cypress)로 실제 브라우저를 띄워 검증한다.
|
|
4
|
+
|
|
5
|
+
*LLM 이 화면을 보고 판단하며 조작하는 인터랙티브 루프*다 — 한 단계 조작 → `screenshot` → 판단 → 다음. `getByRole`/`getByText` 로케이터 = *의미 기반 조작*(좌표 아님), `screenshot` = LLM 이 화면을 봄(mobile 의 agent-device snapshot+ref 와 같은 역할).
|
|
6
|
+
|
|
7
|
+
## 절차
|
|
8
|
+
|
|
9
|
+
1. **serve** — dev 서버를 띄우거나(`npm run dev`) 프로덕션 빌드를 serve. 어느 URL인지 확인.
|
|
10
|
+
2. **launch** — Playwright로 그 URL을 연다:
|
|
11
|
+
```js
|
|
12
|
+
const { chromium } = require('playwright');
|
|
13
|
+
const browser = await chromium.launch();
|
|
14
|
+
const page = await browser.newPage();
|
|
15
|
+
await page.goto(baseURL);
|
|
16
|
+
```
|
|
17
|
+
3. **drive + observe** — 사용자 흐름을 조작(`click`·`fill`·`goto`)하고 `page.screenshot()`로 화면 확인. 반응형이면 `viewport`를 바꿔가며.
|
|
18
|
+
4. **assert / teardown** — 기대 요소·텍스트·네트워크 결과 확인 후 `browser.close()`.
|
|
19
|
+
|
|
20
|
+
## 함정
|
|
21
|
+
|
|
22
|
+
- **서버 준비 대기**: 서버가 뜨기 전 goto 하면 실패 — health 체크나 `waitForLoadState` 후 진행.
|
|
23
|
+
- **flaky 대기**: 고정 `sleep` 대신 `waitFor*`(요소·네트워크 idle)로 기다린다.
|
|
24
|
+
- **결정성**: API 응답을 mock 하거나 시드 데이터로 고정해 화면을 재현 가능하게.
|
|
25
|
+
|
|
26
|
+
> repo에 `playwright.config`/`cypress.config`가 있으면 그 설정·baseURL·스크립트를 그대로 쓴다.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: spec
|
|
3
|
+
description: >-
|
|
4
|
+
새 task/기능에 *착수하기 전*, 요구사항을 *열린 질문이 0이 될 때까지* 인터뷰해 — 실제 코드·시스템에 발 딛고, 추측 없이 —
|
|
5
|
+
단단한 계획(spec)을 만든다. 설렁설렁 계획 = garbage-in→garbage-out 방지. "spec", "스펙 떠줘", "계획 짜자",
|
|
6
|
+
"요구사항 정리", "기획", "이거 어떻게 만들지", "시작 전에 정하자", "뭘 만들지 합의하자" 류, 또는 비자명한 작업을
|
|
7
|
+
*시작*하려 할 때 발동. boss(완료·품질 게이트)의 대칭 *입력 게이트* — 단순 Q&A 답이 아니라 착수 전 tight spec
|
|
8
|
+
산출이 목적. (작고 가역인 변경은 한 문장 intent로 충분 — 무거운 spec 강요 X.)
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# spec — 착수 전 계획을 단단히 (garbage-in 게이트)
|
|
12
|
+
|
|
13
|
+
설렁설렁 세운 계획은 설렁설렁 구현된다(garbage-in → garbage-out). 이 스킬은 *열린 질문이 0이 될 때까지* 인터뷰하고, 실제 시스템에 발을 딛고, 추측 대신 확인으로, 단단한 spec 을 남긴다. **boss(출력 게이트)의 대칭 — 입력 게이트.**
|
|
14
|
+
|
|
15
|
+
**읽는 것:** repo 의 `.claude/playbook.md`(특히 #1 spec-first · #3 재사용 · #4 SSOT · #5 YAGNI · #12 추측 말고 확인 · #13 '최선'·완결성) + 이 repo 의 `CLAUDE.md`·기존 코드 관례.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 원칙 — 왜 이렇게
|
|
20
|
+
|
|
21
|
+
- **추측 금지, 발로 확인 (playbook #12).** 불확실하면 코드를 읽고·돌려보고·웹으로 확인. 시간 민감한 것(버전·API·best practice)은 `date` 로 오늘 확인 + 웹으로 *현재* 검증(네 지식은 stale 할 수 있다). 계획 단계의 추측이 가장 비싼 garbage-in.
|
|
22
|
+
- **'되는' 게 아니라 '최선' (#13).** 매 결정에서 *그냥 되는 방법인가, best practice인가*를 자문하고 검증한다. 시간보다 완결성 — 빨리 대충 말고 철저히.
|
|
23
|
+
- **열린 질문 0.** "나중에 정하자"·"적당히"는 garbage-in. 다 못 박는다.
|
|
24
|
+
- **진공에서 계획 금지.** 닿는 시스템을 먼저 읽고, 거기 위에서 계획한다.
|
|
25
|
+
- **scale 비례(완결성은 유지).** 작고 가역인 일엔 무거운 문서·인터뷰가 불필요(YAGNI) — 단 *이해·검증*은 작은 일이라도 건너뛰지 않는다(시간 아끼려 스킵 금지).
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 실행 (run) — 순서대로
|
|
30
|
+
|
|
31
|
+
### 1. Ground — 닿는 시스템 읽기 (질문 *전에*)
|
|
32
|
+
|
|
33
|
+
이 일이 닿는 코드·모듈·계약·스키마·아키텍처·관례를 먼저 읽는다. 불확실하면 `grep`·실행·웹으로 *확인*(추측 X). 시간 민감한 사실은 `date` + 웹으로 현재 기준 확인. → 그래야 *날카로운* 질문이 나오고, 재사용·SSOT(#3·#4)를 계획에 반영한다.
|
|
34
|
+
|
|
35
|
+
### 2. Interview — 열린 질문이 0이 될 때까지
|
|
36
|
+
|
|
37
|
+
체계적으로 캔다: 스코프(in/out·**non-goal**) · 동작(happy + 엣지 + 에러 경로) · 데이터/계약 · UX · **완료 기준(= 돌릴 수 있는 check)** · 제약 · 리스크. 특히 *사용자가 미처 생각 못 한 어려운 부분*을 파고든다(unknown-unknown 표면화 — 이게 핵심 가치). 구조적 선택은 **AskUserQuestion**(비자명하면 `🤔 모르겠음 → decide` 옵션도 같이). **사용자가 막히면 `decide`** 호출(웹 조사 + 추천). 합의가 수렴(열린 질문 0)할 때까지.
|
|
38
|
+
|
|
39
|
+
### 3. Fit — 기존 + 미래 시스템 같이 고민
|
|
40
|
+
|
|
41
|
+
각 결정마다: 지금 아키텍처에 맞나? *다음 변경이 싼* 구조인가, corner 에 박나? *최선의 접근*인가(불확실하면 웹으로 best practice·deprecated 검증). ⚠️ **speculative 과설계 금지(YAGNI)** — "바뀔 게 확실한 곳을 열어둠"이지 "올지 모를 미래를 미리 빌드"가 아니다. breaking change·마이그 영향(#7)도 플래그.
|
|
42
|
+
|
|
43
|
+
### 4. 산출 — 단단한 계획 문서
|
|
44
|
+
|
|
45
|
+
열린 질문 0 인 구체 spec 을 repo 계획 위치(예: `docs/plans/`)에 남긴다: **목표 · 스코프(+non-goal) · 결정(+why) · 접근 · 완료 기준 · 리스크.** 이게 step 2 개발의 SSOT 이자, 나중에 boss 가 대조할 *intent* 다.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Gotchas (이 스킬이 잘 빠지는 함정)
|
|
50
|
+
|
|
51
|
+
- **추측·낡은 지식을 사실처럼** — 가장 흔한 사고. 불확실/시간민감 = 코드·실행·`date`+웹으로 확인, 못 하면 "미확인"이라 명시.
|
|
52
|
+
- **진공 계획** — 기존 시스템 안 읽고 계획 → 충돌·중복.
|
|
53
|
+
- **열린 질문 잔존** — "나중에" 하나라도 남으면 garbage-in. 0 으로.
|
|
54
|
+
- **요구 날조** — 빠진 건 *사용자가 결정*하게 표면화하지, 내가 멋대로 채우지 않는다.
|
|
55
|
+
- **시간 아끼려 검증·조사 스킵** — 완결성 > 속도(#13). LLM 이 일하니 시간은 핑계가 아니다.
|
|
56
|
+
- **over-spec** — 작은 일에 풀 인터뷰·speculative 설계(YAGNI). 단 이해·검증은 유지한다.
|