leerness 1.9.438 → 1.9.440
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/CHANGELOG.md +27 -0
- package/README.md +95 -408
- package/bin/leerness.js +22 -6
- package/lib/drift.js +32 -29
- package/lib/pure-utils.js +1 -0
- package/package.json +1 -1
- package/scripts/e2e.js +44 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.440 — 2026-06-08 — 🛡️ [안정화/Stable] 시크릿 스캐너 prefix 패턴 placeholder 가드 (12th 외부평가 Opus P2, UR-0140)
|
|
4
|
+
|
|
5
|
+
**🛡️ 안정화 릴리스(Stable) — 보안 스캐너 False-Positive 가 CI 를 깨던 문제 수정.**
|
|
6
|
+
|
|
7
|
+
### 변경 (12th 외부 멀티모델 리뷰 Opus P2)
|
|
8
|
+
- **`scan secrets`/`gate`/`audit` 시크릿 스캐너**: placeholder 가드(`_isPlaceholderSecret`)가 `valueGroup` 있는 2개 패턴에만 적용돼, **AWS/GitHub 등 19개 prefix 패턴은 가드를 우회**(1.9.436 픽스가 그들에겐 dead code). → `.env.example`(placeholder 전용, gitignore 대상 아님)의 더미 토큰(`AKIAXXXX…`/`ghp_XXXX…`)이 커밋 시크릿으로 오탐 → `scan/gate/audit` 실패(exit 1) → CI 파손.
|
|
9
|
+
- **수정**: `valueGroup` 없으면 전체 매치(`m[0]`)로 placeholder 판정 — 19 prefix 패턴도 8연속 더미/마커 가드 적용. `requireSecretLike` 는 valueGroup 한정 유지. **진짜 키(고엔트로피)는 그대로 탐지**(회귀 0), `sk-EXAMPLE…` 류 실키-FN 정책도 유지.
|
|
10
|
+
- 통합 갭 방지: selftest 가 순수 함수만이 아닌 **스캐너 경로**도 검증(Opus 지적), E2E B(1.9.440) 추가(.env.example 더미 미탐 + 진짜 탐지).
|
|
11
|
+
|
|
12
|
+
### 검증 (회귀 0)
|
|
13
|
+
- **selftest 184→185** + **E2E 신규 B(1.9.440)**: 더미 prefix 토큰 미탐(exit 0) + 진짜 AWS 키 탐지(exit 1) + sk-EXAMPLE 실키 유지.
|
|
14
|
+
- 맹신 X: 내 1.9.436 의 불완전성(prefix 패턴 미적용)을 12th 리뷰가 적발, 직접 재현·수정. P3(AWS …EXAMPLE 키 FP)는 기존 FN 정책 충돌로 UR-0144 분리.
|
|
15
|
+
|
|
16
|
+
### 안정화 표시 (R-0006)
|
|
17
|
+
CHANGELOG [안정화/Stable] · git tag/GitHub release (Stable) · npm dist-tag `stable`.
|
|
18
|
+
|
|
19
|
+
## 1.9.439 — 2026-06-08 — drift --auto-fix --json 순수성 (10th 외부평가 Codex P1, UR-0135 완결)
|
|
20
|
+
|
|
21
|
+
**📐 `drift check --auto-fix --json` 이 dirty 워크스페이스에서 진행로그를 stdout 에 섞던 비-순수 JSON 해소.**
|
|
22
|
+
|
|
23
|
+
### 변경
|
|
24
|
+
- **`lib/drift.js`**: `--json` 시 auto-fix 진행로그(보안회복/인코딩/delivered/idempotency/release ~28건)를 `afLog`(no-op)로 무음화. depth 가드(1.9.432)로 재귀가 auto-fix 블록을 건너뛰고 최종 JSON 만 출력 → stdout 순수 JSON 보장. 텍스트 모드는 진행로그 그대로 유지(최종 JSON 출력 `log()` 는 불변).
|
|
25
|
+
|
|
26
|
+
### 검증 (회귀 0)
|
|
27
|
+
- dirty WS(보안 신호 발화) `drift --auto-fix --json` → 순수 JSON 파싱 성공. 텍스트 모드 진행로그 3건 유지.
|
|
28
|
+
- **selftest 183→184** + **E2E 신규 B(1.9.439)**. → 10th 외부평가 Codex P1(--json 전 경로 순수성) 완결.
|
|
29
|
+
|
|
3
30
|
## 1.9.438 — 2026-06-08 — contract impl ESM re-export 인식 (11th 외부평가 Sonnet P3, UR-0139 완결)
|
|
4
31
|
|
|
5
32
|
**🧩 `export { default as X } from './m'` 재export 의 별칭 X 를 named export 로 인식.**
|
package/README.md
CHANGED
|
@@ -1,477 +1,165 @@
|
|
|
1
|
-
#
|
|
1
|
+
# leerness
|
|
2
2
|
|
|
3
|
-
> **AI 코딩 에이전트의
|
|
4
|
-
> **A CLI harness that stops AI coding agents from faking completion, duplicating work, forgetting context, and colliding.**
|
|
3
|
+
> **AI 코딩 에이전트를 위한 운영 레이어(operating layer).** 코드를 대신 쓰는 도구가 아니라, AI 에이전트의 **기억·인수인계·검증·감사·보안 가드**를 프로젝트에 영속화하는 CLI + MCP 서버입니다.
|
|
5
4
|
|
|
6
|
-
[](https://www.npmjs.com/package/leerness) ·  · **런타임 의존성 0** · **install-script 0** · offline-first · Node ≥ 18 · MIT
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
╔══════════════════════════════════════════════════════════════╗
|
|
10
|
-
║ ██╗ ███████╗███████╗██████╗ ███╗ ██╗███████╗███████╗ ║
|
|
11
|
-
║ ██║ ██╔════╝██╔════╝██╔══██╗████╗ ██║██╔════╝██╔════╝ ║
|
|
12
|
-
║ ██║ █████╗ █████╗ ██████╔╝██╔██╗ ██║█████╗ ███████╗ ║
|
|
13
|
-
║ ██║ ██╔══╝ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ╚════██║ ║
|
|
14
|
-
║ ███████╗███████╗███████╗██║ ██║██║ ╚████║███████╗███████║ ║
|
|
15
|
-
║ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝ ║
|
|
16
|
-
║ AI Agent Reliability Harness (버전: 상단 배지 참조) ║
|
|
17
|
-
║ verify · remember · orchestrate · audit · drift · roles ║
|
|
18
|
-
╚══════════════════════════════════════════════════════════════╝
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
<p align="center"><b>🇰🇷 <a href="#한국어">한국어</a> · 🇬🇧 <a href="#english">English</a></b></p>
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
<a id="한국어"></a>
|
|
26
|
-
|
|
27
|
-
# 🇰🇷 한국어
|
|
28
|
-
|
|
29
|
-
## 한눈에 보기
|
|
30
|
-
|
|
31
|
-
`leerness`는 **Claude Code · Cursor · Copilot · Codex · Antigravity · Grok · opencode · Qwen · Aider · Goose** 같은 AI 코딩 에이전트와 함께 일할 때 생기는 구조적 문제를 자동으로 막아주는 CLI 도구입니다.
|
|
32
|
-
|
|
33
|
-
AI는 코드를 빠르게 쓰지만, 다음 **5가지 함정**에 반복해서 빠집니다 — leerness는 이를 모두 자동으로 감지·차단·회복합니다.
|
|
34
|
-
|
|
35
|
-
| # | 함정 | leerness의 방어 |
|
|
36
|
-
|---|------|-----------------|
|
|
37
|
-
| 1 | **거짓 완료** — "구현했습니다"인데 코드 변경/테스트가 없음 | `verify-claim --run-tests` (증거 파일 + 실제 테스트 실행) |
|
|
38
|
-
| 2 | **중복 생성** — 이미 있는 함수를 또 만듦 | `reuse-map` (워크스페이스 전체 중복 감지) |
|
|
39
|
-
| 3 | **망각** — 다음 세션에서 계획·결정을 잃음 | `handoff` (컨텍스트 자동 적재) |
|
|
40
|
-
| 4 | **충돌** — 멀티 에이전트가 같은 파일을 동시에 수정 | `agents dispatch` (경로 격리 + 역할 분배) |
|
|
41
|
-
| 5 | **도구 drift** — 시간이 지나며 도구·메타파일을 잊음 | `drift check` (자동 감지 + 회복) |
|
|
7
|
+
> 이 문서는 **외부 다중 모델(Codex / Claude Sonnet / Claude Opus)이 README 를 보지 않고 leerness 를 직접 설치·실행·소스 분석한 객관 리뷰**를 바탕으로 재구성되었습니다. 매 릴리스마다 자동 갱신됩니다.
|
|
42
8
|
|
|
43
9
|
---
|
|
44
10
|
|
|
45
|
-
##
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
# 1) 설치 + 초기화 — 시스템 언어를 자동 감지해 한/영 가이드 표시 (1.9.269)
|
|
49
|
-
npx leerness@latest init .
|
|
50
|
-
|
|
51
|
-
# 2) AI 세션 시작 — 컨텍스트 자동 적재
|
|
52
|
-
npx leerness handoff .
|
|
11
|
+
## leerness가 뭔가요?
|
|
53
12
|
|
|
54
|
-
|
|
55
|
-
npx leerness verify-claim T-0001 --run-tests
|
|
13
|
+
AI 코딩 에이전트(Claude Code, Cursor, Codex, Aider, Goose 등)는 코드를 잘 쓰지만 세 가지 약점이 있습니다.
|
|
56
14
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
15
|
+
1. **기억하지 못합니다** — 세션이 바뀌면 현재 상태·결정·다음 작업을 잊습니다.
|
|
16
|
+
2. **거짓 완료를 선언합니다** — 증거(파일·테스트·로그) 없이 "완료했습니다"라고 말합니다.
|
|
17
|
+
3. **표준이 없습니다** — 여러 에이전트 간 인수인계, 보안/인코딩 점검, 드리프트 관리가 제각각입니다.
|
|
60
18
|
|
|
61
|
-
|
|
19
|
+
leerness는 이 문제들을 해결하는 **외부 운영 substrate**입니다. 어떤 에이전트 위에도 얹어, 프로젝트의 상태를 `.harness/` 파일로 영속화하고 CLI · MCP 도구로 노출합니다. **leerness 자체는 LLM을 호출하거나 코드를 실행하지 않습니다.**
|
|
62
20
|
|
|
63
21
|
---
|
|
64
22
|
|
|
65
|
-
##
|
|
66
|
-
|
|
67
|
-
### 1. 워크스페이스에 "기억 채널"을 만듭니다
|
|
68
|
-
|
|
69
|
-
```
|
|
70
|
-
.harness/
|
|
71
|
-
├── plan.md ← 무엇을 할 것인가
|
|
72
|
-
├── progress-tracker.md ← 무엇을 했는가 (증거 포함)
|
|
73
|
-
├── decisions.md ← 왜 그렇게 했는가
|
|
74
|
-
└── session-handoff.md ← 다음 세션에 무엇을 넘기는가
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
이 파일들은 **사용자 메모리로 보호**됩니다. leerness가 자동 갱신하되, 사람이나 AI가 직접 쓴 내용은 **항상 보존**되고 변경 전 `.harness/archive/`에 백업됩니다.
|
|
78
|
-
|
|
79
|
-
### 2. AI가 "완료"라고 할 때마다 증거로 검증
|
|
80
|
-
|
|
81
|
-
```
|
|
82
|
-
AI: "T-0042 API 호출 구현 완료했습니다"
|
|
83
|
-
↓ leerness verify-claim T-0042 --run-tests
|
|
84
|
-
evidence 파일 확인 → 테스트 실제 실행 → 코드 매칭
|
|
85
|
-
↓
|
|
86
|
-
✓ 검증 통과 또는 ✗ "코드에 fetch 호출 흔적 없음 (신뢰도 0.85)"
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
### 3. 여러 AI 에이전트를 역할별로 지휘 (1.9.270 신규)
|
|
90
|
-
|
|
91
|
-
모델마다 강점이 다릅니다. **역할을 모델에 매핑**해 적재적소로 일을 시킵니다.
|
|
92
|
-
|
|
93
|
-
```bash
|
|
94
|
-
leerness roles set 코딩 --provider codex --model gpt-5.5 # 코딩 담당
|
|
95
|
-
leerness roles set 검수자 --provider claude --model claude-opus-4-7 # 검수 담당
|
|
96
|
-
leerness roles suggest # 활성 에이전트 기반 최적 배치 + 근거 추천
|
|
97
|
-
leerness agents dispatch "이 변경 검수" --role 검수자 # 역할 → 모델 자동 라우팅
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
> 역할 7종: 지휘(commander)·검수(reviewer)·코딩(coder)·설계(architect)·디자인(designer)·디버그(debugger)·분배(dispatcher). 한국어 별칭 지원.
|
|
101
|
-
|
|
102
|
-
### 4. 라운드가 길어지면 "leerness를 안 쓰는" drift를 자동 감지
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
leerness drift check .
|
|
106
|
-
→ 🔴 critical (110/200) — session close 11일 누락
|
|
107
|
-
→ 권장: leerness session close . (1회 실행으로 🟠 attention 회복)
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
### AGENTS.md(정적) vs leerness(동적) — 보완 관계
|
|
111
|
-
|
|
112
|
-
leerness는 [AGENTS.md](https://agents.md)를 **대체하지 않고 보완**합니다.
|
|
23
|
+
## 해결하는 문제
|
|
113
24
|
|
|
114
|
-
|
|
|
25
|
+
| 문제 | leerness의 해법 |
|
|
115
26
|
|---|---|
|
|
116
|
-
|
|
|
117
|
-
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
## 📊 적용 효과 (정량)
|
|
124
|
-
|
|
125
|
-
`leerness-bench` 28 프로젝트 124 task 측정:
|
|
126
|
-
|
|
127
|
-
| 항목 | 적용 | 미적용 |
|
|
128
|
-
|------|-----:|-------:|
|
|
129
|
-
| 다중 에이전트 효율 | 100/100 | 3/100 |
|
|
130
|
-
| 자동 검수 (verify-claim) | 98/100 | 0/100 |
|
|
131
|
-
| 재사용 인식 | 100/100 | 0/100 |
|
|
132
|
-
| 자동 BUG 감지 | 100/100 | 0/100 |
|
|
133
|
-
| 컨텍스트 유지 | 100/100 | 0/100 |
|
|
134
|
-
| **종합** | **597/600 (99%)** | **3/600 (0.5%)** |
|
|
135
|
-
|
|
136
|
-
- 수동 검수 **90초 → 자동 1.5초**
|
|
137
|
-
- 컨텍스트 적재 **500자** (`handoff --compact`, AI 토큰 90% 절감)
|
|
138
|
-
|
|
139
|
-
---
|
|
140
|
-
|
|
141
|
-
## 🛠️ 핵심 명령
|
|
142
|
-
|
|
143
|
-
```bash
|
|
144
|
-
# 일상
|
|
145
|
-
leerness init [path] # 신규 설치 (시스템 언어 자동 감지)
|
|
146
|
-
leerness handoff [path] [--compact] # 세션 시작 컨텍스트 적재
|
|
147
|
-
leerness verify-claim T-XXX --run-tests # 거짓 완료 검증
|
|
148
|
-
leerness audit [path] [--fix] # 일관성 감사 (--fix 자동 갱신)
|
|
149
|
-
leerness session close [path] # 세션 마감 + 다음 세션 인계
|
|
150
|
-
leerness drift check [path] # leerness 미사용 drift 점수
|
|
151
|
-
|
|
152
|
-
# 멀티 AI 에이전트 · 역할
|
|
153
|
-
leerness setup-agents # 활성화 (claude/codex/agy/grok/opencode/qwen/aider/goose/copilot/ollama)
|
|
154
|
-
leerness agents list | quota # 상태 / 한도
|
|
155
|
-
leerness agents dispatch "<task>" --role coder # 역할 기반 모델 라우팅
|
|
156
|
-
leerness agents bench "<task>" # 여러 CLI 동시 호출 + 비교
|
|
157
|
-
leerness roles list | set | suggest | verify # 모델별 역할 부여
|
|
158
|
-
|
|
159
|
-
# 보안 · 인코딩
|
|
160
|
-
leerness scan secrets . # API 키/토큰 패턴 스캔
|
|
161
|
-
leerness encoding check . # BOM/UTF-16/한글 라운드트립
|
|
162
|
-
leerness gate [path] # verify+audit+scan+encoding+lazy 통합
|
|
163
|
-
|
|
164
|
-
# 버전
|
|
165
|
-
leerness update --check # 새 버전 감지
|
|
166
|
-
leerness update --yes # 자동 마이그레이션 + 검증
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
전체 명령은 `leerness commands`, MCP 도구는 `leerness mcp serve`로 확인하세요.
|
|
170
|
-
|
|
171
|
-
---
|
|
172
|
-
|
|
173
|
-
## ❓ FAQ
|
|
174
|
-
|
|
175
|
-
**Q. leerness가 내 코드를 바꾸나요?**
|
|
176
|
-
A. 사용자 메모리(plan/progress/decisions)는 **항상 보존**, 모든 변경 전 자동 백업합니다. 소스 코드는 직접 수정하지 않습니다.
|
|
177
|
-
|
|
178
|
-
**Q. AI 에이전트 없이도 쓸 수 있나요?**
|
|
179
|
-
A. 네. 1인 개발자의 작업 검증·기억·중복 감지에도 유용합니다.
|
|
180
|
-
|
|
181
|
-
**Q. 외부 AI CLI를 자동으로 호출하나요?**
|
|
182
|
-
A. **절대 아니요.** `agents dispatch`는 실행 명령 텍스트만 생성합니다. 실제 실행은 사용자/메인 에이전트가 명시적으로 합니다.
|
|
183
|
-
|
|
184
|
-
**Q. CI에서 쓸 수 있나요?**
|
|
185
|
-
A. 네. 모든 명령이 exit code + `--json` + `--yes` 비대화형을 지원합니다.
|
|
27
|
+
| 세션 간 맥락·결정·다음 작업 유실 | `task`/`decision`/`lesson`/`plan`/`rule` 메모리를 영속화하고, 세션 시작 시 `handoff` 1콜로 회수 |
|
|
28
|
+
| 증거 없는 "완료" 주장 | `verify-claim --require-evidence`, `lazy detect`, anti-lazy 정책으로 차단 |
|
|
29
|
+
| Claude → Codex → Cursor 교체 시 맥락 소실 | 표준 세션 워크플로(handoff → 작업 → session close)로 에이전트 독립 인수인계 |
|
|
30
|
+
| 하드코딩 시크릿 · 인코딩 깨짐(BOM/CP949) | `scan secrets`, `encoding check` 자동 감지 (CI 게이트) |
|
|
31
|
+
| 워크스페이스 노화(drift)를 에이전트가 모름 | `drift check [--auto-fix]` 점수화 + 자동 회복 |
|
|
32
|
+
| 명세 ↔ 구현 불일치 | `contract verify spec.md impl.js` 함수/필드 일치 검사 |
|
|
33
|
+
| 여러 AI CLI의 역할/분배 표준 부재 | `agents`/`roles`/`team` 오케스트레이션(기본 opt-in, dispatch는 실행이 아닌 명령 생성) |
|
|
186
34
|
|
|
187
35
|
---
|
|
188
36
|
|
|
189
|
-
##
|
|
190
|
-
|
|
191
|
-
leerness 는 권한이 큰 CLI 하네스입니다(child_process · git · 외부 CLI · 자동화 브리지 · hook 설치). **할 수 있는 전부와 끄는 법**을 공개합니다.
|
|
37
|
+
## 빠른 시작
|
|
192
38
|
|
|
193
39
|
```bash
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
- **런타임 의존성 0** · **postinstall 없음** · **변경 전 자동 백업** · **동의 없는 외부 호출 금지**
|
|
198
|
-
- `init` 시 `.claude/settings.local.json` 에 SessionStart hook(`update --check`)이 설치됩니다 → 끄기: `leerness init . --no-auto-update`
|
|
199
|
-
- 회사/운영 코드에서는 먼저 한 프로젝트에서 `init` 후 `git diff` 로 검토하고 커밋하세요.
|
|
200
|
-
- 자세히: [SECURITY.md](SECURITY.md)
|
|
40
|
+
# 설치 (런타임 의존성 0 — 추가 패키지 없음)
|
|
41
|
+
npm install -g leerness
|
|
201
42
|
|
|
202
|
-
|
|
43
|
+
# 프로젝트 초기화 (.harness/ 거버넌스 문서 + .claude/.cursor/.github 어댑터 생성)
|
|
44
|
+
leerness init .
|
|
203
45
|
|
|
204
|
-
|
|
205
|
-
leerness
|
|
206
|
-
leerness init . --minimal # 핵심 파일만 (에디터 통합/가이드/roadmap/.env 생략)
|
|
207
|
-
leerness init . --no-env # .env/.env.example 자동 생성만 생략
|
|
208
|
-
```
|
|
46
|
+
# 세션 시작 — 이전 맥락/기억/다음 작업을 1콜로 회수
|
|
47
|
+
leerness handoff .
|
|
209
48
|
|
|
210
|
-
|
|
49
|
+
# ... 에이전트가 작업 ...
|
|
50
|
+
leerness task add "사용자 인증 API 구현"
|
|
51
|
+
leerness gate . # verify + audit + scan secrets + encoding + lazy 통합 게이트
|
|
211
52
|
|
|
212
|
-
|
|
213
|
-
leerness
|
|
214
|
-
leerness adapter cursor # 2) 내 도구만 (.cursor + .mcp.json) / only my tool
|
|
215
|
-
leerness adapter list # 가능 어댑터 (claude/cursor/codex/goose/opencode/aider/qwen/...)
|
|
216
|
-
# MCP 지원 도구는 .mcp.json 에 leerness 등록 → 상태 verb(state_*) 직접 호출
|
|
53
|
+
# 세션 종료 — 마감 통계 + 다음 라운드 추천 + 인수인계 문서 자동 생성
|
|
54
|
+
leerness session close .
|
|
217
55
|
```
|
|
218
56
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
leerness는 활발히 개발됩니다(잦은 1.9.x). 채널을 골라 안정성을 제어하세요:
|
|
222
|
-
|
|
223
|
-
```bash
|
|
224
|
-
npm i leerness # latest (안정) — 일반 사용자 기본
|
|
225
|
-
npm i leerness@next # next (실험) — 조기 검증용
|
|
226
|
-
npm i leerness@1.9.275 # 버전 고정 — 재현성 (운영 코드 권장)
|
|
227
|
-
leerness release channel # 현재 채널/정책 확인 (--json)
|
|
228
|
-
```
|
|
57
|
+
CLAUDE.md / AGENTS.md 에 `세션 시작 시 leerness handoff .`, `종료 전 leerness session close .` 지침을 추가하면 에이전트가 자동으로 호출합니다.
|
|
229
58
|
|
|
230
59
|
---
|
|
231
60
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
# 🇬🇧 English
|
|
235
|
-
|
|
236
|
-
## At a glance
|
|
237
|
-
|
|
238
|
-
`leerness` is a CLI harness that automatically prevents the structural problems that arise when working with AI coding agents like **Claude Code · Cursor · Copilot · Codex · Antigravity · Grok · opencode · Qwen · Aider · Goose**.
|
|
239
|
-
|
|
240
|
-
AI writes code fast, but repeatedly falls into **5 traps** — leerness detects, blocks, and recovers from all of them.
|
|
61
|
+
## 핵심 개념 — 5계층
|
|
241
62
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
| 4 | **Collisions** — multiple agents editing the same file | `agents dispatch` (path isolation + role routing) |
|
|
248
|
-
| 5 | **Tool drift** — forgetting tools/meta files over time | `drift check` (auto detection + recovery) |
|
|
63
|
+
- **기억(Memory)** — `task`/`plan`/`decision`/`lesson`/`rule`/`feature`(그래프). canonical JSON 을 단일 진실소스로 저장하고 마크다운은 projection. archive/restore 지원.
|
|
64
|
+
- **인수인계(Handoff)** — `handoff`(세션 시작 컨텍스트), `session close`(마감 보고). 에이전트 교체에도 맥락 보존.
|
|
65
|
+
- **검증(Verification)** — `verify-claim`(증거 강제), `contract verify`(명세↔구현), `verify-code`(테스트/빌드 실행 + 증거 기록).
|
|
66
|
+
- **감사(Audit)** — `scan secrets`, `encoding check`, `lazy detect`, `drift check`, `audit`, 그리고 이를 묶는 `gate`.
|
|
67
|
+
- **정책/보안(Policy)** — 8단계 권한 등급 enforce, 외부 에이전트 opt-in, 자연어 영구 룰(`rule add ... --trigger`).
|
|
249
68
|
|
|
250
69
|
---
|
|
251
70
|
|
|
252
|
-
##
|
|
71
|
+
## 명령 카테고리
|
|
253
72
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
npx leerness verify-claim T-0001 --run-tests
|
|
263
|
-
|
|
264
|
-
# 4) Close the session — hand off to the next one
|
|
265
|
-
npx leerness session close .
|
|
266
|
-
```
|
|
73
|
+
- **초기화/진단**: `init` · `status` · `which` · `doctor` · `selftest` · `capabilities`
|
|
74
|
+
- **메모리**: `task` · `plan` · `decision` · `lesson` · `rule` · `feature` · `memory status/search`
|
|
75
|
+
- **인수인계/세션**: `handoff` · `session close` · `pulse` · `health`
|
|
76
|
+
- **검증/감사**: `check` · `gate` · `audit` · `drift check` · `lazy detect` · `scan secrets` · `encoding check` · `verify-code` · `verify-claim` · `contract verify`
|
|
77
|
+
- **외부 에이전트**: `agents list/check/dispatch` · `provider` · `roles` · `adapter`
|
|
78
|
+
- **운영/확장**: `release` · `migrate` · `team` · `install-safety` · `route` · `review`(페르소나)
|
|
79
|
+
- **브리지(opt-in)**: `web`(playwright) · `pc`(robotjs) · `lsp`
|
|
80
|
+
- **MCP**: `mcp serve` — stdio JSON-RPC 서버로 85개 도구 노출
|
|
267
81
|
|
|
268
|
-
|
|
82
|
+
전체 명령은 `leerness commands` 또는 `leerness --help` 로 확인하세요.
|
|
269
83
|
|
|
270
84
|
---
|
|
271
85
|
|
|
272
|
-
##
|
|
273
|
-
|
|
274
|
-
### 1. Creates "memory channels" in your workspace
|
|
275
|
-
|
|
276
|
-
```
|
|
277
|
-
.harness/
|
|
278
|
-
├── plan.md ← what to do
|
|
279
|
-
├── progress-tracker.md ← what was done (with evidence)
|
|
280
|
-
├── decisions.md ← why it was done that way
|
|
281
|
-
└── session-handoff.md ← what to pass to the next session
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
These are **protected as user memory**. leerness updates them automatically, but anything a human or AI wrote is **always preserved** and backed up to `.harness/archive/` before any change.
|
|
285
|
-
|
|
286
|
-
### 2. Verifies every "done" with evidence
|
|
287
|
-
|
|
288
|
-
```
|
|
289
|
-
AI: "Finished implementing the T-0042 API call"
|
|
290
|
-
↓ leerness verify-claim T-0042 --run-tests
|
|
291
|
-
check evidence files → actually run tests → match code
|
|
292
|
-
↓
|
|
293
|
-
✓ verified or ✗ "no trace of a fetch call in code (confidence 0.85)"
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
### 3. Directs multiple AI agents by role (new in 1.9.270)
|
|
297
|
-
|
|
298
|
-
Each model has different strengths. **Map roles to models** to put the right model on the right job.
|
|
86
|
+
## 대표 워크플로
|
|
299
87
|
|
|
88
|
+
**기본 세션 사이클**
|
|
300
89
|
```bash
|
|
301
|
-
leerness
|
|
302
|
-
leerness
|
|
303
|
-
leerness
|
|
304
|
-
leerness
|
|
90
|
+
leerness handoff . # 이전 맥락 자동 로드
|
|
91
|
+
leerness task add "API 응답 검증 로직 구현"
|
|
92
|
+
leerness task update T-0001 --status in-progress --evidence "검증 함수 구현"
|
|
93
|
+
leerness gate . # 배포 전 통합 품질 게이트
|
|
94
|
+
leerness session close . # 마감 + 다음 에이전트 인수인계
|
|
305
95
|
```
|
|
306
96
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
### 4. Detects "drift" when you stop using leerness over long rounds
|
|
310
|
-
|
|
97
|
+
**보안·검증 게이트 (CI)**
|
|
311
98
|
```bash
|
|
312
|
-
leerness
|
|
313
|
-
|
|
314
|
-
|
|
99
|
+
leerness scan secrets . # 커밋 대상 하드코딩 시크릿 → exit 1
|
|
100
|
+
leerness contract verify spec.md src/api.js # 명세 함수/필드 누락 → exit 1
|
|
101
|
+
leerness verify-claim T-0001 --require-evidence
|
|
315
102
|
```
|
|
316
103
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
leerness **complements, not replaces** [AGENTS.md](https://agents.md).
|
|
320
|
-
|
|
321
|
-
| Static — AGENTS.md | Dynamic — leerness |
|
|
322
|
-
|---|---|
|
|
323
|
-
| coding rules, test commands, prohibitions, deploy steps (rarely change) | current goal, files changed, failed attempts, verification, next handoff (change every task) |
|
|
324
|
-
| written by humans | recorded to `.leerness/` via `leerness state` / MCP `leerness_state_*` |
|
|
325
|
-
|
|
326
|
-
→ Rules live in AGENTS.md; **work state/memory/verification/handoff live in leerness** (the shared operating layer for any agent).
|
|
327
|
-
|
|
328
|
-
---
|
|
329
|
-
|
|
330
|
-
## 📊 Measured impact
|
|
331
|
-
|
|
332
|
-
From `leerness-bench` across 28 projects / 124 tasks:
|
|
333
|
-
|
|
334
|
-
| Metric | With | Without |
|
|
335
|
-
|--------|-----:|--------:|
|
|
336
|
-
| Multi-agent efficiency | 100/100 | 3/100 |
|
|
337
|
-
| Auto-verification (verify-claim) | 98/100 | 0/100 |
|
|
338
|
-
| Reuse awareness | 100/100 | 0/100 |
|
|
339
|
-
| Auto bug detection | 100/100 | 0/100 |
|
|
340
|
-
| Context retention | 100/100 | 0/100 |
|
|
341
|
-
| **Total** | **597/600 (99%)** | **3/600 (0.5%)** |
|
|
342
|
-
|
|
343
|
-
- Manual review **90s → 1.5s automated**
|
|
344
|
-
- Context load **~500 chars** (`handoff --compact`, ~90% fewer AI tokens)
|
|
345
|
-
|
|
346
|
-
---
|
|
347
|
-
|
|
348
|
-
## 🛠️ Core commands
|
|
349
|
-
|
|
104
|
+
**다중 에이전트 조율**
|
|
350
105
|
```bash
|
|
351
|
-
#
|
|
352
|
-
leerness
|
|
353
|
-
leerness handoff [path] [--compact] # load session-start context
|
|
354
|
-
leerness verify-claim T-XXX --run-tests # verify fake completion
|
|
355
|
-
leerness audit [path] [--fix] # consistency audit (--fix auto-update)
|
|
356
|
-
leerness session close [path] # close session + hand off
|
|
357
|
-
leerness drift check [path] # leerness-disuse drift score
|
|
358
|
-
|
|
359
|
-
# Multi AI agents · roles
|
|
360
|
-
leerness setup-agents # enable (claude/codex/agy/grok/opencode/qwen/aider/goose/copilot/ollama)
|
|
361
|
-
leerness agents list | quota # status / limits
|
|
362
|
-
leerness agents dispatch "<task>" --role coder # role-based model routing
|
|
363
|
-
leerness agents bench "<task>" # call several CLIs at once + compare
|
|
364
|
-
leerness roles list | set | suggest | verify # per-model role assignment
|
|
365
|
-
|
|
366
|
-
# Security · encoding
|
|
367
|
-
leerness scan secrets . # API key/token pattern scan
|
|
368
|
-
leerness encoding check . # BOM/UTF-16/CJK round-trip
|
|
369
|
-
leerness gate [path] # verify+audit+scan+encoding+lazy combined
|
|
370
|
-
|
|
371
|
-
# Versions
|
|
372
|
-
leerness update --check # detect new version
|
|
373
|
-
leerness update --yes # auto-migrate + verify
|
|
106
|
+
leerness agents list # 설치된 외부 AI CLI 가용성
|
|
107
|
+
leerness agents dispatch "코드 리뷰" --to codex # 실행 명령 생성(직접 실행은 사용자/메인 에이전트)
|
|
374
108
|
```
|
|
375
109
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
---
|
|
379
|
-
|
|
380
|
-
## ❓ FAQ
|
|
381
|
-
|
|
382
|
-
**Q. Does leerness change my code?**
|
|
383
|
-
A. User memory (plan/progress/decisions) is **always preserved** and backed up before any change. It never edits your source code directly.
|
|
384
|
-
|
|
385
|
-
**Q. Can I use it without an AI agent?**
|
|
386
|
-
A. Yes. Solo developers can use it for self-verification, memory, and duplicate detection.
|
|
387
|
-
|
|
388
|
-
**Q. Does it auto-call external AI CLIs?**
|
|
389
|
-
A. **Never.** `agents dispatch` only generates command text. Actual execution is explicit, by you or your main agent.
|
|
390
|
-
|
|
391
|
-
**Q. Can I use it in CI?**
|
|
392
|
-
A. Yes. Every command supports exit codes, `--json`, and non-interactive `--yes`.
|
|
393
|
-
|
|
394
|
-
---
|
|
395
|
-
|
|
396
|
-
## 🔒 Security & transparency
|
|
397
|
-
|
|
398
|
-
leerness is a powerful CLI harness (child_process, git, external CLIs, automation bridges, hook install). It discloses **everything it can do and how to turn each off**.
|
|
399
|
-
|
|
110
|
+
**MCP (외부 AI 에이전트에 도구로 노출)**
|
|
400
111
|
```bash
|
|
401
|
-
leerness
|
|
112
|
+
leerness mcp serve # JSON-RPC over stdio, 85 도구
|
|
402
113
|
```
|
|
403
114
|
|
|
404
|
-
|
|
405
|
-
- `init` installs a SessionStart hook (`update --check`) in `.claude/settings.local.json` → disable with `leerness init . --no-auto-update`
|
|
406
|
-
- For company/production code, trial `init` in one project, review with `git diff`, then commit.
|
|
407
|
-
- Details: [SECURITY.md](SECURITY.md)
|
|
408
|
-
|
|
409
|
-
**Lighter-install options (1.9.276):**
|
|
410
|
-
|
|
411
|
-
```bash
|
|
412
|
-
leerness init . --dry-run # preview files to be created/modified (zero changes)
|
|
413
|
-
leerness init . --minimal # core files only (skips editor integrations/guides/roadmap/.env)
|
|
414
|
-
leerness init . --no-env # skip only .env/.env.example auto-creation
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
### Release channels (stable vs experimental)
|
|
115
|
+
---
|
|
418
116
|
|
|
419
|
-
|
|
117
|
+
## 아키텍처 (외부 리뷰 검증)
|
|
420
118
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
119
|
+
- **런타임 의존성 0 / install-script 0** — `package.json` 의 dependencies/optional/peer 가 전부 비어 있고 postinstall 도 없습니다. 순수 Node stdlib(`fs`/`path`/`child_process`/`readline`). 공급망 공격면 최소. `leerness install-safety` 로 확인 가능.
|
|
120
|
+
- **canonical JSON 단일 진실소스 + 마크다운 projection** — 메모리는 JSON 으로 저장하고 사람이 읽는 `.md` 는 파생물. 파이프(`|`)·개행·백틱·이모지·한글이 마크다운 테이블에서도 안전(셀 이스케이프 + round-trip).
|
|
121
|
+
- **원자적 UTF-8 쓰기** — temp + rename 으로 부분쓰기 손상 방지, BOM 자동 strip.
|
|
122
|
+
- **shell 미경유 MCP** — `mcp serve` 의 도구 호출은 셸을 거치지 않고 인자를 직접 전달(명령 주입 차단).
|
|
123
|
+
- **순수 `--json` / 일관 exit code** — 성공·실패·미존재 경로 모두 파싱 가능한 JSON(`{ok,error,code}`) + 실패 시 exit 1. 자동화·CI 친화.
|
|
124
|
+
- **모듈 분리(DI)** + **내장 자가검증** — `lib/` 순수 유틸/IO/카탈로그 분리, `selftest` 180+ 케이스로 설치 무결성 검증.
|
|
427
125
|
|
|
428
126
|
---
|
|
429
127
|
|
|
430
|
-
##
|
|
128
|
+
## 차별점
|
|
431
129
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
130
|
+
- **진짜 0-dependency** — SaaS 가 아니라 파일 기반 로컬 운영 메모리. 네트워크 불필요(업데이트 확인 제외).
|
|
131
|
+
- **에이전트 중립** — Claude / Codex / Cursor / Aider / Goose 등 어디에나 적용.
|
|
132
|
+
- **거짓 완료 방지** — 완료 주장에 evidence 를 요구하는 anti-lazy 흐름.
|
|
133
|
+
- **통합 게이트** — 보안·인코딩·드리프트·검증을 `gate` 하나로.
|
|
134
|
+
- **한국어 우선 + Windows/인코딩 1급 시민** — CP949/BOM 가드, 한국어 출력 기본(`--language en` 지원).
|
|
135
|
+
- **자기기술(self-describing)** — `about`/`commands`/`capabilities`/`doctor` 로 도구가 스스로 설명.
|
|
136
|
+
- **지속적 외부 검증** — 다중 모델 클린룸 리뷰로 회귀 0 을 유지하며 진화.
|
|
439
137
|
|
|
440
138
|
---
|
|
441
139
|
|
|
442
|
-
##
|
|
140
|
+
## 보안
|
|
443
141
|
|
|
444
|
-
-
|
|
445
|
-
-
|
|
446
|
-
-
|
|
447
|
-
- **1.9.268** — grok 정식 provider 승격 / grok promoted to a first-class provider.
|
|
448
|
-
- **1.9.265~267** — CLI 에이전트 슬래시 명령 레지스트리 + `--help` probe 자동 갱신.
|
|
449
|
-
|
|
450
|
-
> 전체 이력 / full history: [CHANGELOG.md](CHANGELOG.md)
|
|
142
|
+
- 시크릿/키/토큰을 소스에 하드코딩하지 마세요. `.env` 사용 + `.gitignore` 포함. `scan secrets`/`gate` 가 커밋 대상 시크릿을 차단합니다.
|
|
143
|
+
- 외부 AI CLI 연동은 **기본 비활성**입니다(`LEERNESS_ENABLE_*` opt-in). 브리지(web/pc/lsp)도 opt-in.
|
|
144
|
+
- 자세한 권한 표면은 [SECURITY.md](./SECURITY.md) 참고.
|
|
451
145
|
|
|
452
146
|
---
|
|
453
147
|
|
|
454
|
-
##
|
|
455
|
-
|
|
456
|
-
- Issues: https://github.com/gugu9999gu/leerness/issues
|
|
457
|
-
- PRs: e2e 통과 + 한국어 주석 + UTF-8 (no BOM) / pass e2e, Korean comments, UTF-8 without BOM.
|
|
458
|
-
- 테스트 / Tests:
|
|
459
|
-
```bash
|
|
460
|
-
npm run test:fast # 빠른 핵심-경로 smoke (selftest + 13 체크, ~10초) / fast core-path smoke (~10s)
|
|
461
|
-
npm test # 전체 게이트 (selftest + e2e 220+) / full gate
|
|
462
|
-
```
|
|
148
|
+
## 링크
|
|
463
149
|
|
|
464
|
-
|
|
150
|
+
- 홈페이지: https://leerness.com
|
|
151
|
+
- npm: https://www.npmjs.com/package/leerness
|
|
152
|
+
- GitHub: https://github.com/gugu9999gu/leerness
|
|
153
|
+
- 변경 이력: [CHANGELOG.md](./CHANGELOG.md)
|
|
465
154
|
|
|
466
|
-
|
|
155
|
+
## 라이선스
|
|
467
156
|
|
|
468
|
-
|
|
469
|
-
> **A tool that makes AI agents work reliably. It never auto-calls external LLMs/APIs/CLIs without your consent.**
|
|
157
|
+
MIT
|
|
470
158
|
|
|
471
159
|
<!-- leerness:project-readme:start -->
|
|
472
160
|
## Leerness Project Harness
|
|
473
161
|
|
|
474
|
-
이 프로젝트는 Leerness v1.9.
|
|
162
|
+
이 프로젝트는 Leerness v1.9.440 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
475
163
|
|
|
476
164
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
477
165
|
|
|
@@ -525,7 +213,7 @@ leerness memory restore decision <date|title>
|
|
|
525
213
|
|
|
526
214
|
### MCP server (외부 AI 통합)
|
|
527
215
|
|
|
528
|
-
Leerness v1.9.
|
|
216
|
+
Leerness v1.9.440는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
|
|
529
217
|
|
|
530
218
|
```jsonc
|
|
531
219
|
// 카테고리별
|
|
@@ -546,7 +234,7 @@ Leerness v1.9.438는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
546
234
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
547
235
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push (1.9.140+) → 7) session close → 8) 다음 라운드 예약.
|
|
548
236
|
|
|
549
|
-
현재 누적: **70 라운드 (1.9.40 → 1.9.
|
|
237
|
+
현재 누적: **70 라운드 (1.9.40 → 1.9.440)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
550
238
|
|
|
551
239
|
### 성능 가이드 (1.9.140 측정)
|
|
552
240
|
|
|
@@ -584,6 +272,5 @@ leerness release pack --close --auto-main-push
|
|
|
584
272
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
585
273
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
586
274
|
|
|
587
|
-
Last synced by Leerness v1.9.
|
|
275
|
+
Last synced by Leerness v1.9.440: 2026-06-08
|
|
588
276
|
<!-- leerness:project-readme:end -->
|
|
589
|
-
|
package/bin/leerness.js
CHANGED
|
@@ -31,7 +31,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
|
|
|
31
31
|
// 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
|
|
32
32
|
const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_CATALOG, _LSP_LANG_PATTERNS, OPTIMISM_PATTERNS, BUILT_IN_PERSONAS, STRINGS, BUILTIN_CATALOG, ROADMAP_STATUS_LABEL, ROADMAP_STATUS_COLOR, SECRET_PATTERNS, MERGE_OVERWRITE_FILES, MINIMAL_SKIP_KEYS, REQUIRED_WORKSPACE_FILES, KEYWORD_STOPWORDS, SKILL_CATALOG_PRESETS } = require('../lib/catalogs'); // 1.9.344/368/369 (UR-0025): catalog 분리 (MERGE_OVERWRITE_FILES/MINIMAL_SKIP_KEYS 포함)
|
|
33
33
|
|
|
34
|
-
const VERSION = '1.9.
|
|
34
|
+
const VERSION = '1.9.440';
|
|
35
35
|
|
|
36
36
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
37
37
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -3291,6 +3291,22 @@ function _selfTestCases() {
|
|
|
3291
3291
|
const noDefault = JSON.stringify(m._parseImplExports('export default function x(){}')) === JSON.stringify([]);
|
|
3292
3292
|
return reexp && named && noDefault;
|
|
3293
3293
|
} },
|
|
3294
|
+
{ name: '10th 외부평가 Codex P1 (UR-0135): drift --auto-fix --json 진행로그 억제(순수 JSON) (1.9.439)', run: () => {
|
|
3295
|
+
const modSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'drift.js'));
|
|
3296
|
+
const afLogDef = modSrc.includes("const afLog = has('--json') ? () => {} : log;");
|
|
3297
|
+
const usesAfLog = (modSrc.match(/afLog\(/g) || []).length >= 10; // auto-fix 진행로그 다수 afLog 화
|
|
3298
|
+
const jsonStillLog = modSrc.includes('log(JSON.stringify({ root, score'); // 최종 JSON 출력은 log 유지
|
|
3299
|
+
return afLogDef && usesAfLog && jsonStillLog;
|
|
3300
|
+
} },
|
|
3301
|
+
{ name: '12th 외부평가 Opus P2 (UR-0140 stab): 시크릿 스캐너 prefix 패턴도 placeholder 가드 적용 (1.9.440)', run: () => {
|
|
3302
|
+
const src = read(__filename);
|
|
3303
|
+
// 스캐너가 valueGroup 없을 때 m[0] 로 placeholder 판정(prefix 패턴 가드)
|
|
3304
|
+
const wired = src.includes('const val = (valueGroup != null) ? m[valueGroup] : m[0];') && src.includes('if (_isPlaceholderSecret(val)) {');
|
|
3305
|
+
// 행위: prefix 8연속 더미는 placeholder, 진짜(고엔트로피)는 real (FN 정책 유지 — 'example' 포함 실키는 real)
|
|
3306
|
+
const m = require('../lib/pure-utils');
|
|
3307
|
+
const behav = m._isPlaceholderSecret('AKIA' + 'X'.repeat(16)) === true && m._isPlaceholderSecret('AKIAJQXMP7RZ2KL9WXYZ') === false && m._isPlaceholderSecret('sk-EXAMPLEab12cd34ef56gh78ij90kl') === false;
|
|
3308
|
+
return wired && behav;
|
|
3309
|
+
} },
|
|
3294
3310
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
3295
3311
|
];
|
|
3296
3312
|
}
|
|
@@ -7116,11 +7132,11 @@ function _collectSecretFindings(root) {
|
|
|
7116
7132
|
let m;
|
|
7117
7133
|
while ((m = re.exec(text))) {
|
|
7118
7134
|
// 1.9.365 (CV-6/UR-0081): placeholder/예시 값 스킵.
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
}
|
|
7135
|
+
// 1.9.440 (12th 외부평가 Opus P2): prefix 패턴(valueGroup 없음, AWS/GitHub 등 19종)도 placeholder 가드 적용 —
|
|
7136
|
+
// 기존엔 valueGroup 있는 2패턴만 가드 → .env.example 의 더미 토큰(AKIAXXXX…/ghp_XXXX…)이 커밋 시크릿 오탐(CI 파손). 전체 매치(m[0])로 판정.
|
|
7137
|
+
const val = (valueGroup != null) ? m[valueGroup] : m[0];
|
|
7138
|
+
if (_isPlaceholderSecret(val)) { if (re.lastIndex === m.index) re.lastIndex++; continue; }
|
|
7139
|
+
if (valueGroup != null && requireSecretLike && !_looksSecretLike(val)) { if (re.lastIndex === m.index) re.lastIndex++; continue; }
|
|
7124
7140
|
const line = text.slice(0, m.index).split('\n').length;
|
|
7125
7141
|
findings.push({ file: fileRel, line, name, snippet: m[0].slice(0, 32), gitignored });
|
|
7126
7142
|
break;
|
package/lib/drift.js
CHANGED
|
@@ -163,25 +163,28 @@ function driftCheckCmd(root, opts = {}, deps = {}) {
|
|
|
163
163
|
// 1.9.432 (10th 외부평가 Opus latent, UR-0131 잔여): depth 가드 — 재귀 호출(_noAutoFix)은 auto-fix 재진입 금지.
|
|
164
164
|
// 기존엔 autoFix=has('--auto-fix')가 전역 argv 재독→재귀도 auto-fix 분기 재진입, 종료는 'audit이 보안신호를 지운다'는 취약 불변식에 의존(미래 신호 타입이 비가역이면 무한재귀). 명시 1회 보장.
|
|
165
165
|
const autoFix = has('--auto-fix') && !opts._noAutoFix;
|
|
166
|
+
// 1.9.439 (10th 외부평가 Codex P1, UR-0135): --json 모드면 auto-fix 진행로그 억제(stdout 순수 JSON 보장).
|
|
167
|
+
// 재귀(_noAutoFix)는 auto-fix 블록을 건너뛰고 마지막 JSON(아래 has('--json') 블록)만 출력 → afLog 로 첫 패스 진행로그만 무음화.
|
|
168
|
+
const afLog = has('--json') ? () => {} : log;
|
|
166
169
|
// 1.9.82: 보안 신호가 fired에 있으면 우선 audit --fix 호출
|
|
167
170
|
const hasSecurityFired = fired.some(f => /보안 위험 \(1\.9\.78\)/.test(f.label));
|
|
168
171
|
if (autoFix && hasSecurityFired) {
|
|
169
|
-
|
|
170
|
-
|
|
172
|
+
afLog('');
|
|
173
|
+
afLog(`🔒 --auto-fix 활성 (1.9.82) — 보안 신호 회복: audit --fix 자동 실행 중...`);
|
|
171
174
|
try {
|
|
172
175
|
const r = cp.spawnSync(process.execPath, [harnessPath, 'audit', root, '--fix'],
|
|
173
176
|
{ encoding: 'utf8', timeout: 30000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' } });
|
|
174
177
|
if (r.status === 0) {
|
|
175
|
-
|
|
178
|
+
afLog(`✓ audit --fix 완료 — .gitignore + .env.example 동기화`);
|
|
176
179
|
// 재검사 (보안 신호 회복 확인)
|
|
177
|
-
|
|
178
|
-
|
|
180
|
+
afLog('');
|
|
181
|
+
afLog(`재검사 중...`);
|
|
179
182
|
return driftCheckCmd(root, { ...opts, _noAutoFix: true }, deps); // 재귀 1회 (auto-fix 없이, 1.9.432 depth 가드)
|
|
180
183
|
} else {
|
|
181
|
-
|
|
184
|
+
afLog(`⚠ audit --fix 실패 (exit ${r.status}) — 수동 \`leerness audit --fix\` 권장`);
|
|
182
185
|
}
|
|
183
186
|
} catch (e) {
|
|
184
|
-
|
|
187
|
+
afLog(`⚠ auto-fix 보안 회복 오류: ${e.message}`);
|
|
185
188
|
}
|
|
186
189
|
}
|
|
187
190
|
// 1.9.242: drift check --auto-fix 에 env encoding BOM 자동 추가 통합 (사용자 명시 UR-0014 2단계)
|
|
@@ -190,8 +193,8 @@ function driftCheckCmd(root, opts = {}, deps = {}) {
|
|
|
190
193
|
try {
|
|
191
194
|
const encScan = _scanShellScriptsEncoding(root);
|
|
192
195
|
if (encScan.atRisk && encScan.atRisk.length > 0) {
|
|
193
|
-
|
|
194
|
-
|
|
196
|
+
afLog('');
|
|
197
|
+
afLog(`🌐 --auto-fix 활성 (1.9.242) — 셸 스크립트 인코딩 위험 ${encScan.atRisk.length}건 BOM 자동 추가 중...`);
|
|
195
198
|
let ok = 0;
|
|
196
199
|
for (const r of encScan.atRisk) {
|
|
197
200
|
try {
|
|
@@ -203,10 +206,10 @@ function driftCheckCmd(root, opts = {}, deps = {}) {
|
|
|
203
206
|
ok++;
|
|
204
207
|
} catch {}
|
|
205
208
|
}
|
|
206
|
-
|
|
209
|
+
afLog(`✓ UTF-8 BOM 추가 ${ok}/${encScan.atRisk.length}건 (1.9.242 UR-0014)`);
|
|
207
210
|
}
|
|
208
211
|
} catch (e) {
|
|
209
|
-
|
|
212
|
+
afLog(`⚠ env encoding auto-fix 오류 (1.9.242): ${e.message}`);
|
|
210
213
|
}
|
|
211
214
|
}
|
|
212
215
|
// 1.9.225: drift check --auto-fix 에 delivered 패턴 자동 적용 통합 (1.9.223/224 시스템 회수)
|
|
@@ -216,17 +219,17 @@ function driftCheckCmd(root, opts = {}, deps = {}) {
|
|
|
216
219
|
try {
|
|
217
220
|
const delivered = _detectDeliveredRequests(root);
|
|
218
221
|
if (delivered.candidates && delivered.candidates.length > 0) {
|
|
219
|
-
|
|
220
|
-
|
|
222
|
+
afLog('');
|
|
223
|
+
afLog(`📥 --auto-fix 활성 (1.9.225) — delivered 패턴 ${delivered.candidates.length}건 자동 완료 중...`);
|
|
221
224
|
let ok = 0;
|
|
222
225
|
for (const c of delivered.candidates) {
|
|
223
226
|
const u = _updateUserRequest(root, c.id, { status: 'completed', autoCompletedAt: new Date().toISOString(), autoCompleteReason: 'drift-auto-fix-1.9.225' });
|
|
224
227
|
if (u) ok++;
|
|
225
228
|
}
|
|
226
|
-
|
|
229
|
+
afLog(`✓ delivered 자동 완료 ${ok}/${delivered.candidates.length}건`);
|
|
227
230
|
}
|
|
228
231
|
} catch (e) {
|
|
229
|
-
|
|
232
|
+
afLog(`⚠ delivered auto-apply 오류 (1.9.225): ${e.message}`);
|
|
230
233
|
}
|
|
231
234
|
}
|
|
232
235
|
// 1.9.293: drift check --auto-fix 에 idempotency task/user-request 중복 자동 정리 통합
|
|
@@ -236,11 +239,11 @@ function driftCheckCmd(root, opts = {}, deps = {}) {
|
|
|
236
239
|
const idemFixes = _autoFixIdempotency(root);
|
|
237
240
|
const totalFixed = idemFixes.reduce((n, f) => n + (f.removedExact || 0) + (f.droppedSameText || 0) + (f.count || 0), 0);
|
|
238
241
|
if (totalFixed > 0) {
|
|
239
|
-
|
|
240
|
-
|
|
242
|
+
afLog('');
|
|
243
|
+
afLog(`🔁 --auto-fix 활성 (1.9.293) — idempotency 중복 ${totalFixed}건 자동 정리 (task/user-request dedup)`);
|
|
241
244
|
}
|
|
242
245
|
} catch (e) {
|
|
243
|
-
|
|
246
|
+
afLog(`⚠ idempotency auto-fix 오류 (1.9.293): ${e.message}`);
|
|
244
247
|
}
|
|
245
248
|
}
|
|
246
249
|
// 1.9.236: drift check --auto-fix 에 release cleanup 통합 (1.9.235 회수)
|
|
@@ -255,8 +258,8 @@ function driftCheckCmd(root, opts = {}, deps = {}) {
|
|
|
255
258
|
.map(l => l.replace(/^\*?\s+/, '').trim())
|
|
256
259
|
.filter(l => l && /^release\/\d+\.\d+\.\d+$/.test(l));
|
|
257
260
|
if (merged.length > 50) {
|
|
258
|
-
|
|
259
|
-
|
|
261
|
+
afLog('');
|
|
262
|
+
afLog(`🗑 --auto-fix 활성 (1.9.236) — release/* merged ${merged.length}개 (50+) 자동 정리 (keep 10)...`);
|
|
260
263
|
// 정렬 (semver desc)
|
|
261
264
|
merged.sort((a, b) => {
|
|
262
265
|
const va = a.replace('release/', '').split('.').map(n => parseInt(n, 10) || 0);
|
|
@@ -272,20 +275,20 @@ function driftCheckCmd(root, opts = {}, deps = {}) {
|
|
|
272
275
|
const r = cp.spawnSync('git', ['branch', '-d', b], { cwd: root, encoding: 'utf8' });
|
|
273
276
|
if (r.status === 0) ok++;
|
|
274
277
|
}
|
|
275
|
-
|
|
278
|
+
afLog(`✓ release cleanup 자동 완료 ${ok}/${toDelete.length}건 (keep 10)`);
|
|
276
279
|
}
|
|
277
280
|
}
|
|
278
281
|
} catch (e) {
|
|
279
|
-
|
|
282
|
+
afLog(`⚠ release cleanup auto-fix 오류 (1.9.236): ${e.message}`);
|
|
280
283
|
}
|
|
281
284
|
}
|
|
282
285
|
if (autoFix && level === '🔴 critical' && !hasSecurityFired) {
|
|
283
|
-
|
|
284
|
-
|
|
286
|
+
afLog('');
|
|
287
|
+
afLog(`🔧 --auto-fix 활성 — session close 자동 실행 중...`);
|
|
285
288
|
try {
|
|
286
289
|
const r = cp.spawnSync(process.execPath, [harnessPath, 'session', 'close', root], { encoding: 'utf8', timeout: 60000, env: { ...process.env, LEERNESS_INTERNAL: '1' } });
|
|
287
290
|
if (r.status === 0) {
|
|
288
|
-
|
|
291
|
+
afLog(`✓ session close 자동 완료`);
|
|
289
292
|
// autoResolved 카운트
|
|
290
293
|
const stats = _readUsageStats(root);
|
|
291
294
|
stats.drift = stats.drift || {};
|
|
@@ -294,14 +297,14 @@ function driftCheckCmd(root, opts = {}, deps = {}) {
|
|
|
294
297
|
mkdirp(path.dirname(p));
|
|
295
298
|
writeUtf8(p, JSON.stringify(stats, null, 2) + '\n');
|
|
296
299
|
// 재검사
|
|
297
|
-
|
|
298
|
-
|
|
300
|
+
afLog('');
|
|
301
|
+
afLog(`재검사 중...`);
|
|
299
302
|
return driftCheckCmd(root, { ...opts, _noAutoFix: true }, deps); // 재귀 1회 (auto-fix 없이, 1.9.432 depth 가드)
|
|
300
303
|
} else {
|
|
301
|
-
|
|
304
|
+
afLog(`⚠ session close 실패 (exit ${r.status}) — 수동 실행 필요`);
|
|
302
305
|
}
|
|
303
306
|
} catch (e) {
|
|
304
|
-
|
|
307
|
+
afLog(`⚠ auto-fix 오류: ${e.message}`);
|
|
305
308
|
}
|
|
306
309
|
}
|
|
307
310
|
if (has('--json')) {
|
package/lib/pure-utils.js
CHANGED
|
@@ -636,6 +636,7 @@ function _isPlaceholderSecret(value) {
|
|
|
636
636
|
const hasRealPrefix = /^(?:sk-|sk-proj-|pk_|rk_|akia|ghp_|gho_|ghs_|ghr_|github_pat_|xox[baprs]-|aiza|ya29\.|glpat-|-----begin)/.test(v);
|
|
637
637
|
// 1.9.436 (11th 외부평가 Opus P3): prefix 가 있어도 본문이 동일문자 8+연속(AKIAXXXX…/…00000000…)이면 명백한 더미 → placeholder. 실키는 고엔트로피라 무영향.
|
|
638
638
|
if (/(.)\1{7,}/.test(alnum)) return true;
|
|
639
|
+
// (12th 외부평가 Opus P3 'AWS …EXAMPLE 키'는 보류 — 'example' 통째 placeholder 화는 기존 FN 정책(sk-EXAMPLE… 실키 유지, UR-0105)과 충돌. UR-0144 로 분리.)
|
|
639
640
|
// 실키 prefix → 항상 실키(마커 무시). 그 외 마커 단어 있으면 placeholder(고엔트로피여도). prefix 없고 마커 없고 고엔트로피 → 실키.
|
|
640
641
|
if (hasRealPrefix) return false;
|
|
641
642
|
if (hasMarker) return true;
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -37,7 +37,7 @@ run('scan secrets', ['scan', 'secrets', tmp]);
|
|
|
37
37
|
run('encoding check', ['encoding', 'check', tmp]);
|
|
38
38
|
|
|
39
39
|
const secretFile = path.join(tmp, 'fake-config.json');
|
|
40
|
-
fs.writeFileSync(secretFile, JSON.stringify({ openai: 'sk-' + '
|
|
40
|
+
fs.writeFileSync(secretFile, JSON.stringify({ openai: 'sk-' + 'a1B2'.repeat(12) }));
|
|
41
41
|
run('scan secrets (detect)', ['scan', 'secrets', tmp], { expectFail: true });
|
|
42
42
|
fs.unlinkSync(secretFile);
|
|
43
43
|
|
|
@@ -2695,7 +2695,7 @@ total++;
|
|
|
2695
2695
|
total++;
|
|
2696
2696
|
{
|
|
2697
2697
|
fs.mkdirSync(path.join(tmp, '_devspace'), { recursive: true });
|
|
2698
|
-
fs.writeFileSync(path.join(tmp, '_devspace/secret-config.js'), `const k = "ghp_${'
|
|
2698
|
+
fs.writeFileSync(path.join(tmp, '_devspace/secret-config.js'), `const k = "ghp_${'a1B2'.repeat(9)}";\n`);
|
|
2699
2699
|
fs.writeFileSync(path.join(tmp, '.leerness-skip-dirs'), '_devspace/\n# 주석은 무시\n');
|
|
2700
2700
|
const r = cp.spawnSync(process.execPath, [CLI, 'scan', 'secrets', tmp], { encoding: 'utf8' });
|
|
2701
2701
|
const ok = r.status === 0 && /no obvious secret patterns/.test(r.stdout);
|
|
@@ -3650,7 +3650,7 @@ total++;
|
|
|
3650
3650
|
let ok = false;
|
|
3651
3651
|
try {
|
|
3652
3652
|
const sd = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-secmod-'));
|
|
3653
|
-
const A = '
|
|
3653
|
+
const A = 'a1B2'.repeat(10);
|
|
3654
3654
|
const lines = [
|
|
3655
3655
|
'const a = "' + 'sk-' + 'proj-' + A + '_' + A + '";', // modern OpenAI project (기존 패턴 놓침)
|
|
3656
3656
|
'const b = "' + 'sk-' + 'ant-api03-' + A + '_' + A + '";', // Anthropic api03 (언더스코어 — 기존 놓침)
|
|
@@ -4434,7 +4434,7 @@ total++;
|
|
|
4434
4434
|
let ok = false;
|
|
4435
4435
|
try {
|
|
4436
4436
|
const c = require(path.resolve(__dirname, '..', 'lib', 'catalogs.js'));
|
|
4437
|
-
const A = '
|
|
4437
|
+
const A = 'a1B2'.repeat(10);
|
|
4438
4438
|
const hit = (s) => c.SECRET_PATTERNS.some(p => { p.re.lastIndex = 0; return p.re.test(s); });
|
|
4439
4439
|
const catOk = Array.isArray(c.SECRET_PATTERNS) && c.SECRET_PATTERNS.length === 20
|
|
4440
4440
|
&& hit('AKIA' + 'ABCD1234EFGH5678') && hit('sk-' + 'ant-api03-' + A + '_' + A) && !hit('const u = "john' + '_doe_2024";');
|
|
@@ -4634,7 +4634,7 @@ total++;
|
|
|
4634
4634
|
fs.rmSync(skf, { recursive: true, force: true });
|
|
4635
4635
|
// UR-0060: 사용자 harness.js 파일도 스캔(false-neg 제거) + 신규 GitLab 패턴 탐지
|
|
4636
4636
|
const ud = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-scan-'));
|
|
4637
|
-
fs.writeFileSync(path.join(ud, 'harness.js'), 'const k = "glpat-' + '
|
|
4637
|
+
fs.writeFileSync(path.join(ud, 'harness.js'), 'const k = "glpat-' + 'a1B2'.repeat(5) + '";\n');
|
|
4638
4638
|
const scr = cp.spawnSync(process.execPath, [CLI, 'scan', 'secrets', ud], { encoding: 'utf8', timeout: 20000 });
|
|
4639
4639
|
const scanOk = /GitLab PAT/.test((scr.stdout || '') + (scr.stderr || ''));
|
|
4640
4640
|
fs.rmSync(ud, { recursive: true, force: true });
|
|
@@ -4736,7 +4736,7 @@ total++;
|
|
|
4736
4736
|
// scan secrets <file> (이전 ENOTDIR) + basename 표시
|
|
4737
4737
|
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-p3-'));
|
|
4738
4738
|
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
4739
|
-
fs.writeFileSync(path.join(d, 'leak.js'), 'const k = "glpat-' + '
|
|
4739
|
+
fs.writeFileSync(path.join(d, 'leak.js'), 'const k = "glpat-' + 'a1B2'.repeat(5) + '";\n');
|
|
4740
4740
|
const fr = cp.spawnSync(process.execPath, [CLI, 'scan', 'secrets', path.join(d, 'leak.js')], { encoding: 'utf8', timeout: 20000 });
|
|
4741
4741
|
const fout = (fr.stdout || '') + (fr.stderr || '');
|
|
4742
4742
|
const fileScanOk = /GitLab PAT/.test(fout) && /leak\.js/.test(fout) && !/ENOTDIR/.test(fout);
|
|
@@ -6073,5 +6073,43 @@ total++;
|
|
|
6073
6073
|
if (!ok) failed++;
|
|
6074
6074
|
}
|
|
6075
6075
|
|
|
6076
|
+
// 1.9.439 (10th 외부평가 Codex P1, UR-0135): drift --auto-fix --json 은 dirty WS 에서도 stdout 순수 JSON.
|
|
6077
|
+
total++;
|
|
6078
|
+
{
|
|
6079
|
+
let ok = false;
|
|
6080
|
+
try {
|
|
6081
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-djson-'));
|
|
6082
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
6083
|
+
fs.writeFileSync(path.join(d, '.env'), 'API_KEY=sk-test-1234567890abcdefghijklmnopqrstuvwxyz\n');
|
|
6084
|
+
fs.writeFileSync(path.join(d, '.gitignore'), 'node_modules/\n'); // .env 누락 → 보안 신호 발화 → auto-fix 진행로그
|
|
6085
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'drift', 'check', d, '--auto-fix', '--json'], { encoding: 'utf8', timeout: 30000 });
|
|
6086
|
+
let pure = false; try { const j = JSON.parse(r.stdout); pure = typeof j.score === 'number'; } catch {}
|
|
6087
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
6088
|
+
ok = pure;
|
|
6089
|
+
} catch {}
|
|
6090
|
+
console.log(ok ? '✓ B(1.9.439) UR-0135: drift --auto-fix --json 순수 JSON(dirty WS 진행로그 억제)' : '✗ drift --auto-fix --json 비순수');
|
|
6091
|
+
if (!ok) failed++;
|
|
6092
|
+
}
|
|
6093
|
+
|
|
6094
|
+
// 1.9.440 (12th 외부평가 Opus P2): 시크릿 스캐너 prefix 패턴(AWS/GitHub)도 placeholder 가드 — .env.example 더미는 미탐, 진짜는 탐지.
|
|
6095
|
+
total++;
|
|
6096
|
+
{
|
|
6097
|
+
let ok = false;
|
|
6098
|
+
try {
|
|
6099
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-scanph-'));
|
|
6100
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
6101
|
+
// 더미 prefix 토큰(.env.example, gitignore 대상 아님) → 미탐(exit 0)
|
|
6102
|
+
fs.writeFileSync(path.join(d, '.env.example'), 'AWS_KEY=AKIA' + 'X'.repeat(16) + '\nGH=ghp_' + 'X'.repeat(36) + '\n');
|
|
6103
|
+
const dummy = cp.spawnSync(process.execPath, [CLI, 'scan', 'secrets', d], { encoding: 'utf8', timeout: 20000 });
|
|
6104
|
+
// 진짜 AWS 키(AKIA+16 랜덤) → 탐지(exit 1)
|
|
6105
|
+
fs.writeFileSync(path.join(d, 'real.js'), 'const k="AKIAJQXMP7RZ2KL9WXYZ";');
|
|
6106
|
+
const real = cp.spawnSync(process.execPath, [CLI, 'scan', 'secrets', d], { encoding: 'utf8', timeout: 20000 });
|
|
6107
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
6108
|
+
ok = dummy.status === 0 && real.status === 1;
|
|
6109
|
+
} catch {}
|
|
6110
|
+
console.log(ok ? '✓ B(1.9.440) UR-0140: 시크릿 스캐너 prefix 더미 미탐 + 진짜 탐지(placeholder 가드 통합)' : '✗ 시크릿 스캐너 prefix 가드 실패');
|
|
6111
|
+
if (!ok) failed++;
|
|
6112
|
+
}
|
|
6113
|
+
|
|
6076
6114
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
6077
6115
|
if (failed > 0) process.exit(1);
|