leerness 1.16.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +140 -0
- package/README.md +6 -6
- package/bin/leerness.js +211 -48
- package/lib/pure-utils.js +1 -1
- package/lib/session-close.js +17 -1
- package/package.json +1 -1
- package/scripts/e2e.js +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,145 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.18.0 — 2026-06-10 — 🛡️ [안정화/Stable] 범용 검증 하네스 안정 minor
|
|
4
|
+
|
|
5
|
+
**🛡️ 안정화(Stable) minor — "범용 AI 코딩 하네스" 실증 격차 전부 해소.** 5개 독립 클린룸 실사용 평가(Python/Node/Rust 실개발·에이전트 교대·적대 공격)가 찾은 P1 2건 + P2 3건(1.17.2~1.17.6)을 검증·통합해 npm 공개. R-0011 정책의 9번째 minor. 새 포지셔닝: **"어떤 언어, 어떤 AI 에이전트로 작업하든 — 증거 없이는 끝났다고 말할 수 없게."**
|
|
6
|
+
|
|
7
|
+
### 이번 minor 통합 (1.17.1~1.17.6)
|
|
8
|
+
- **🌐 테스트 명령 해석 체인 (P1)**: `verify-claim --run-tests` 가 npm test 하드코딩 → `--test-cmd` > 설정 `testCommand` > 실제 npm test(placeholder 제외) > skip. 파이썬 프로젝트 오판 해소 + pytest 출력 파싱.
|
|
9
|
+
- **🛡️ 빈껍데기·가짜 테스트 차단 (P1)**: 주석뿐 구현(비주석 코드 0줄) done 게이팅 FAIL + 테스트-구현 연결 검사(12개 언어, 파이썬 `#` 스텁 포함). `--json implementationSubstance/testImplLink`.
|
|
10
|
+
- **🧪 테스트 카운트 정직화 (P2)**: pytest·루트 `*.test.*` 관례 인식 + 측정불가="검증 미수행"(통과 위장 금지) + 한국어 "테스트 N개" 주장 추출(부풀린 주장 차단).
|
|
11
|
+
- **🔗 unknown flag 거부 (P2)**: `task update --next-action` 같은 미존재 옵션이 값을 조용히 버리던 것 → 거부 + did-you-mean(인수인계 유실 차단).
|
|
12
|
+
- **🔚 마감 정합 (P2)**: session close 가 done 낙관 의심·살아있는 시크릿을 재확인해 표면화 — 마감이 마지막 관문 역할.
|
|
13
|
+
- **plan add 공백제목 손상 수정** + milestone 파서 개행 미흡수(1.17.1).
|
|
14
|
+
|
|
15
|
+
### 검증 (회귀 0)
|
|
16
|
+
- **selftest 222 PASS** · **E2E 365/365 PASS** · npm gate=minor_bump. 각 격차 행위 재현(공격 차단 + 정직 작업 과탐 0) 완료.
|
|
17
|
+
|
|
18
|
+
### 안정화 표시 (R-0006)
|
|
19
|
+
CHANGELOG [안정화/Stable] · git tag (Stable) · GitHub release (Stable) · npm dist-tag `stable` 시도.
|
|
20
|
+
|
|
21
|
+
## 1.17.6 — 2026-06-10 — 범용성⑤(P2 완결): session close 마감 정합 (UR-0049)
|
|
22
|
+
|
|
23
|
+
**🔚 마감이 "마지막 관문" 역할을 하도록.** 5축 실증에서 — verify-claim 이 거짓으로 판정했던 task 가 마감 보고서에 평범한 "done"으로, gate 가 시크릿으로 실패 중인데 "clean (sleep 안전)"으로 선언되던 정합 문제 해소. **이로써 5축 격차(P1 2건 + P2 3건) 전부 닫힘.**
|
|
24
|
+
|
|
25
|
+
### 변경 (UR-0049)
|
|
26
|
+
- **done 낙관 재확인**: 마감 시 done task 들의 evidence 를 코드 흔적과 재대조(`_detectOptimism`) — verify-claim 을 건너뛴 거짓 주장(예: "DB 저장 구현" 인데 코드에 DB 호출 없음)을 `⚠ 낙관 의심 미해소` 로 표면화. `--json completionHonesty.optimismUnresolved`.
|
|
27
|
+
- **마감 보안 재확인**: 커밋 대상 시크릿이 살아있으면 `🚨 마감 보안: N건 미해소 — clean 아님` 표면화. `--json closeSecurity.committedSecrets`.
|
|
28
|
+
- advisory(차단 X) — 마감 흐름은 유지하되 거짓/위험이 조용히 통과하지 않음.
|
|
29
|
+
|
|
30
|
+
### 검증 (회귀 0)
|
|
31
|
+
- **selftest 221→222** · **E2E 365/365** · 행위 3종: 거짓 DB 주장 done + 시크릿 → 두 경고 표면화 ✓ · --json 필드 ✓ · 진짜 구현+시크릿 제거 → 경고 0(과탐 0) ✓.
|
|
32
|
+
- patch(1.17.6) — npm 미배포(R-0011, GitHub). **다음: 1.18.0 [Stable] "범용 검증 하네스" minor(1.17.1~1.17.6 묶음 공개).**
|
|
33
|
+
|
|
34
|
+
## 1.17.5 — 2026-06-10 — 범용성④: 모르는 옵션 조용히 무시 차단 (UR-0048)
|
|
35
|
+
|
|
36
|
+
**🔗 인수인계 유실의 최악 양식 차단.** 5축 실증에서 — `task update --next-action "..."` 처럼 존재하지 않는 옵션을 줘도 "✓ task updated"만 출력하고 값을 버려, **쓴 에이전트는 기록됐다고 믿고 다음 에이전트는 placeholder 를 받던** P2 해소.
|
|
37
|
+
|
|
38
|
+
### 변경 (UR-0048)
|
|
39
|
+
- **`task update` unknown flag 거부**: 지원 외 `--플래그` 발견 시 exit 1 + **did-you-mean**(`--next-action` → "혹시 `--next`?") + 지원 플래그/사용법 안내. `--json` 은 `{code:'unknown_flag'}` 구조화.
|
|
40
|
+
- 전역 플래그(`--path/--json/--force/--lenient`)는 항상 허용 — 과탐 0. 재사용 가능한 `_rejectUnknownFlags` 헬퍼(다른 쓰기 명령 확대 여지).
|
|
41
|
+
|
|
42
|
+
### 검증 (회귀 0)
|
|
43
|
+
- **selftest 220→221** · **E2E 365/365** · 행위 4종: `--next-action` 거부+제안 ✓ · `--json` 구조화 ✓ · 올바른 `--next` 정상 저장 ✓ · 전역 플래그 통과 ✓.
|
|
44
|
+
- patch(1.17.5) — npm 미배포(R-0011, GitHub). 잔여: UR-0049(마감 정합) → 완료 시 1.18.0 "범용 검증" minor.
|
|
45
|
+
|
|
46
|
+
## 1.17.4 — 2026-06-10 — 범용성③: 테스트 카운트 "측정실패=통과" 역전 해소 (UR-0047)
|
|
47
|
+
|
|
48
|
+
**🧪 5개 클린룸 리포트가 공통 지적한 P2.** "실측: 테스트 파일 못 찾음"이라면서 바로 아래 "✓ pass (실측 ≥ 주장)"로 통과시키던 모순 — 부풀린 테스트 주장("cargo test 12개", "테스트 50개")이 검증을 헛통과하던 것 해소.
|
|
49
|
+
|
|
50
|
+
### 변경 (UR-0047)
|
|
51
|
+
- **관례 확대**: 주장된 테스트 파일 우선 합산 + 글롭(pytest `test_*.py`·`*_test.py` / 루트·tests/ 의 `*.test.*`·`*.spec.*`) — 이전엔 `tests/test.js` 등 3개 하드코딩. 파이썬은 `def test_` 개수로 카운트.
|
|
52
|
+
- **측정불가 = 검증 미수행**: 테스트 파일을 못 찾으면 "⊘ 측정 불가 — 주장 N개 검증 미수행 (pass 아님)" 정직 표기 + verdict `testCountMatch: null`(게이팅 미기여 — false 만 FAIL).
|
|
53
|
+
- **🐛 주장 추출 보강(맹신 X 행위검증 중 발견)**: "테스트 50개"(한국어 자연어순)를 못 잡아 **부풀린 주장이 카운트 검증을 아예 안 탔음** → `테스트 N개` 패턴 추가.
|
|
54
|
+
|
|
55
|
+
### 검증 (회귀 0)
|
|
56
|
+
- **selftest 219→220** · **E2E 365/365** · 행위 4종: pytest 실측(2개) ✓ · **부풀린 "테스트 50개"(실측 2) exit 1 차단**(이전 헛통과) · 측정불가 정직 표기 ✓ · 루트 `*.test.js` 인식(실측 3) ✓ · 정직 주장 exit 0(과탐 0).
|
|
57
|
+
- patch(1.17.4) — npm 미배포(R-0011, GitHub). 잔여: UR-0048(unknown flag)·UR-0049(마감 정합).
|
|
58
|
+
|
|
59
|
+
## 1.17.3 — 2026-06-10 — 범용성②: 빈껍데기 구현·가짜 테스트 차단 (UR-0046)
|
|
60
|
+
|
|
61
|
+
**🛡️ 간판 기능 "거짓완료 차단"의 상한선 끌어올리기.** 5축 실증 분석의 Attack C — 주석뿐인 구현 파일 + 아무것도 검사하지 않는 `assert(true)` 테스트가 `verify-claim` 을 **exit 0 으로 완전 통과**하던 P1 — 을 차단.
|
|
62
|
+
|
|
63
|
+
### 변경 (UR-0046)
|
|
64
|
+
- **구현 실체(스텁) 검사**: 주장된 구현 파일(테스트 제외, js/ts/py/go/rs 등 12종 확장자)의 **비주석 코드줄이 0** 이면 done 게이팅 FAIL — "주석/TODO 뿐인 빈껍데기"를 확실 신호만으로 차단(과탐 0).
|
|
65
|
+
- **테스트-구현 연결 검사**: 구현+테스트가 함께 주장됐는데 어떤 테스트도 구현 파일명을 참조하지 않으면 "빈 테스트 의심" advisory ⚠ (`--strict-claims` 시 FAIL).
|
|
66
|
+
- `--json` verdict 에 `implementationSubstance` / `testImplLink` + `stubFiles` 노출(머신 경로 동일 게이팅).
|
|
67
|
+
|
|
68
|
+
### 검증 (회귀 0)
|
|
69
|
+
- **selftest 218→219** · **E2E 365/365** · Attack C 재현: 스텁+가짜테스트 **exit 0→1 차단** · 진짜 구현 exit 0(과탐 0) · **파이썬 스텁(#주석뿐)도 차단**(언어중립) · --json verdict 노출.
|
|
70
|
+
- patch(1.17.3) — npm 미배포(R-0011, GitHub). 잔여 격차: UR-0047(테스트카운트 역전)·0048(unknown flag)·0049(마감 정합).
|
|
71
|
+
|
|
72
|
+
## 1.17.2 — 2026-06-10 — 범용성①: verify-claim 테스트 명령 해석 체인 (UR-0045)
|
|
73
|
+
|
|
74
|
+
**🌐 "범용 AI 코딩 하네스" 5축 실증 분석의 P1 1호 해소.** `verify-claim --run-tests` 가 `npm test` 하드코딩이라 — 테스트가 전부 통과한 파이썬 프로젝트(npm init 잔재 placeholder 존재)를 "주장 불일치 FAIL"로 오판하던 범용성 정면 반례를 수정.
|
|
75
|
+
|
|
76
|
+
### 변경 (UR-0045)
|
|
77
|
+
- **테스트 명령 해석 체인**: `--test-cmd "<명령>"` > `.harness/leerness-config.json` 의 `"testCommand"` > package.json 의 **실제** test 스크립트(npm placeholder "no test specified" 는 테스트가 아니므로 제외) > **skip 표기**(불일치 판정 금지).
|
|
78
|
+
- **pytest 출력 파싱**: "N passed in …s" 형식 인식(파이썬 러너 pass/fail 비율 표시).
|
|
79
|
+
- 신규 value-flag `--test-cmd` 를 `nonFlagArgs` withValue 에 등록(1.14.2 교훈 적용 — 제목 흡수 차단).
|
|
80
|
+
|
|
81
|
+
### 검증 (회귀 0)
|
|
82
|
+
- **selftest 217→218** · **E2E 365/365** · 행위 재현: ① placeholder-only 파이썬 프로젝트 `--run-tests` → skip + exit 0(오판 해소, 이전 exit 1) ② `--test-cmd "node mytest.js"` → 실행+파싱(2/2 passed)+주장 대조 ✓ ③ config testCommand 경유 동작 ④ 실제 npm test 스크립트는 기존대로 ⑤ 실패 명령은 정직하게 exit 1.
|
|
83
|
+
- patch(1.17.2) — npm 미배포(R-0011, GitHub). 다음: UR-0046(스텁/연결 검사 — 빈껍데기 구현 차단).
|
|
84
|
+
|
|
85
|
+
## 1.17.1 — 2026-06-09 — 17번째 버그헌트: plan add 공백제목 데이터 손상 차단
|
|
86
|
+
|
|
87
|
+
**🔬 17번째 버그헌트(이번 세션 신규코드 회귀 + 광역 스윕).** 신규코드(gate --json stdout-swap·memory --json·_GROUP_USAGE·scan break·_cellSafe)는 **0 결함**(5000키 664ms 등 입증). plan add 입력검증 1건 발견·수정.
|
|
88
|
+
|
|
89
|
+
### 수정 (재현 검증)
|
|
90
|
+
- **🟠 plan add 공백 제목 → plan.md 손상 (P2)**: `plan add " "`(공백만)이 `|| 기본값`을 우회(truthy)해 빈 제목으로 기록 → milestone 파서의 `\s*` 가 개행을 흡수해 **다음 줄 `Status: planned` 를 제목으로 오인**. 2중 수정: ① dispatch 에서 `.trim()` 후 판정(공백→기본값 '새 계획'), ② 파서 `\.\s*` → `\.[ \t]*`(개행 미흡수, 5곳 + pure-utils) — 손상 클래스 차단.
|
|
91
|
+
|
|
92
|
+
### 검증 (회귀 0)
|
|
93
|
+
- **selftest 216→217** · **E2E 365/365** · `plan add " "` → '새 계획'(손상 없음)·정상 제목 보존 행위 재현.
|
|
94
|
+
- patch(1.17.1) — npm 미배포(R-0011, GitHub). (P3 plan add 빈-인자 기본값은 의도된 동작 — trim 으로 안전화.)
|
|
95
|
+
|
|
96
|
+
## 1.17.0 — 2026-06-09 — 🛡️ [안정화/Stable] 외부 클린룸 일관성 안정 minor
|
|
97
|
+
|
|
98
|
+
**🛡️ 안정화(Stable) minor.** 외부 클린룸 리뷰(게시본 무README 신규사용자 관점)에서 도출한 --json·CLI 일관성 개선(1.16.1~1.16.2)을 검증·통합해 npm 공개. R-0011 정책의 8번째 minor. 영상은 HyperFrames "문제→해소" 디자인.
|
|
99
|
+
|
|
100
|
+
### 이번 minor 통합 (1.16.1~1.16.2)
|
|
101
|
+
- **gate `--json` 단일 객체화**: 텍스트+단계JSON 혼재로 파싱 불가하던 것 → `{ok,total,failed,checks}` 단일 JSON(CI/에이전트 소비 가능).
|
|
102
|
+
- **memory search `--json`**: 플래그 무시하던 것 → `{query,total,results}` 구조화.
|
|
103
|
+
- **명령그룹 무인자 일관화**: `rule`/`skill`/`feature`/`memory` 를 하위명령 없이 부르면 "알 수 없는 명령" 대신 **사용법 힌트**(decision/lesson 과 일관, `--json` 구조화).
|
|
104
|
+
- **문서 정합**: `about` 의 메모리 경로 `.leerness/` → `.harness/`(기본 워크스페이스) 정정.
|
|
105
|
+
|
|
106
|
+
### 검증 (회귀 0)
|
|
107
|
+
- **selftest 216 PASS** · **E2E 365/365 PASS** · npm gate=minor_bump. gate/memory --json valid JSON + bare-group 사용법 힌트 행위 재현. (맹신 X: scan --json exit·AKIA 는 비-버그로 판정해 제외.)
|
|
108
|
+
|
|
109
|
+
### 안정화 표시 (R-0006)
|
|
110
|
+
CHANGELOG [안정화/Stable] · git tag (Stable) · GitHub release (Stable) · npm dist-tag `stable` 시도.
|
|
111
|
+
|
|
112
|
+
## 1.16.2 — 2026-06-09 — CLI 일관성: 명령그룹 무인자 → 사용법 힌트 (외부클린룸 UR-0042)
|
|
113
|
+
|
|
114
|
+
**🧭 명령그룹을 하위명령 없이 부르면 친절한 사용법 안내.** 외부 클린룸 리뷰가 지적: `rule`/`skill`/`feature`/`memory` 를 하위명령 없이 부르면 "알 수 없는 명령"(유효 그룹인데 혼란)이 떴음 — `decision`/`lesson` 처럼 사용법 힌트로 통일.
|
|
115
|
+
|
|
116
|
+
### 변경 (UR-0042)
|
|
117
|
+
- bare `rule`/`skill`/`feature`/`memory` → `subcommand_required` + 실제 하위명령 사용법 표시(예: `memory search "<키>" | memory status | …`). `--json` 도 구조화 출력.
|
|
118
|
+
- 기존 동작 보존: 하위명령이 있는 호출(`memory search` 등)은 그대로.
|
|
119
|
+
|
|
120
|
+
### 검증 (회귀 0)
|
|
121
|
+
- **selftest 215→216** · **E2E 365/365** · 4개 그룹 bare 호출 → 사용법 힌트, `memory --json` → `{code:'subcommand_required'}` 행위 재현.
|
|
122
|
+
- patch(1.16.2) — npm 미배포(R-0011, GitHub). 잔여: bare `leerness` 자동 init·hook(UR-0041, 설계 재검토), `--language en` 런타임(UR-0042 잔여).
|
|
123
|
+
|
|
124
|
+
## 1.16.1 — 2026-06-09 — 외부 클린룸 리뷰: --json 일관성 + 문서 정합
|
|
125
|
+
|
|
126
|
+
**🔬 외부 클린룸 리뷰(게시본 1.16.0 설치·README/소스 미참조·신규 사용자 관점, 2모델).** 발굴된 지적을 직접 재현해 진짜만 수정(맹신 X — 비-버그/의도된 동작은 제외).
|
|
127
|
+
|
|
128
|
+
### 수정 (전부 재현 검증)
|
|
129
|
+
- **gate `--json` 단일 객체화 (C2)**: 이전엔 텍스트 헤더 + 단계별 JSON 이 섞여 파싱 불가. 이제 `{ok, total, failed, checks:[{name,ok}]}` 단일 객체(하위 출력 억제). CI/에이전트 소비 가능.
|
|
130
|
+
- **memory search `--json` (C3)**: `--json` 을 무시하고 텍스트만 내던 것 → `{query, total, results:[{file,line,text}]}` 구조화(전 명령 일관).
|
|
131
|
+
- **about 문서 정합 (C4)**: `about` 의 메모리 설명이 `.leerness/` 라 했으나 기본 워크스페이스는 `.harness/` → 정정(선택 state substrate 만 .leerness/).
|
|
132
|
+
|
|
133
|
+
### 비-버그 판정 (맹신 X — 재현했으나 수정 안 함)
|
|
134
|
+
- **scan secrets `--json` exit 코드**: 리뷰는 exit 0 라 했으나 재현 결과 committed 발견 시 exit 1 정상(코드도 `if(committed.length) process.exitCode=1`). gitignored-only(안전) → exit 0 은 의도된 동작.
|
|
135
|
+
- **AWS AKIA 미탐지**: `...EXAMPLE` 포함은 placeholder 가드로 스킵(의도) — 실 키(EXAMPLE 없음)는 정상 탐지.
|
|
136
|
+
|
|
137
|
+
### 잔여(백로그): bare `leerness`(무인자) 자동 init·hook / `--language en` 런타임 미적용 / 서브명령그룹 무인자 동작 일관화.
|
|
138
|
+
|
|
139
|
+
### 검증 (회귀 0)
|
|
140
|
+
- **selftest 214→215** · **E2E 365/365** · gate/memory --json valid JSON + about .harness 행위 재현.
|
|
141
|
+
- patch(1.16.1) — npm 미배포(R-0011, GitHub).
|
|
142
|
+
|
|
3
143
|
## 1.16.0 — 2026-06-09 — 🛡️ [안정화/Stable] 16번째 버그헌트 안정 minor
|
|
4
144
|
|
|
5
145
|
**🛡️ 안정화(Stable) minor.** 16번째 멀티에이전트 버그헌트(8건 발굴·전부 직접 재현)의 코어 수정(1.15.1)을 검증·통합해 npm 공개. R-0011 정책의 7번째 minor. **이 릴리스 영상부터 개선된 "문제→해소" 디자인**(짧은 인트로 + 이전/이제 모션 + 의미 보존 하이라이트)으로 제작됩니다.
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
>
|
|
12
|
+
> **어떤 언어, 어떤 AI 에이전트로 작업하든 — "증거 없이는 끝났다고 말할 수 없게" 만드는 AI 코딩 운영 레이어.** 코드를 대신 쓰는 도구가 아니라, AI 에이전트의 **기억·인수인계·검증·감사·보안 가드**를 프로젝트에 영속화하는 CLI + MCP 서버입니다. (이 포지셔닝은 5개 독립 클린룸 실사용 평가 — Python/Node/Rust 실개발·에이전트 교대·적대 공격 — 로 검증됐습니다.)
|
|
13
13
|
|
|
14
14
|
[](https://www.npmjs.com/package/leerness) ·  · **런타임 의존성 0** · **install-script 0** · offline-first · Node ≥ 18 · MIT
|
|
15
15
|
|
|
@@ -186,13 +186,13 @@ MIT
|
|
|
186
186
|
<!-- leerness:project-readme:start -->
|
|
187
187
|
## Leerness Project Harness
|
|
188
188
|
|
|
189
|
-
이 프로젝트는 Leerness v1.
|
|
189
|
+
이 프로젝트는 Leerness v1.18.0 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
190
190
|
|
|
191
191
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
192
192
|
|
|
193
193
|
Leerness 는 **실행기/코딩 에이전트가 아니라**, 어떤 AI 코딩 에이전트(Claude Code · Codex · Cursor · Goose 등) 위에도 얹는 **범용 운영 레이어**입니다. 5개 공통 계층을 제공합니다:
|
|
194
194
|
|
|
195
|
-
- **기억(Memory)** — 프로젝트 상태/결정/진행을 `.
|
|
195
|
+
- **기억(Memory)** — 프로젝트 상태/결정/진행을 `.harness/` 에 영속화
|
|
196
196
|
- **정책(Policy)** — 8단계 권한 등급 + enforce (read-only→publish), MCP 호출 게이트
|
|
197
197
|
- **인수인계(Handoff)** — 에이전트 간 컨텍스트 표준 전달 + `get_project_context` 1콜 온보딩
|
|
198
198
|
- **검증(Verification)** — 근거 기반 완료 검증으로 허위 완료 차단
|
|
@@ -240,7 +240,7 @@ leerness memory restore decision <date|title>
|
|
|
240
240
|
|
|
241
241
|
### MCP server (외부 AI 통합)
|
|
242
242
|
|
|
243
|
-
Leerness v1.
|
|
243
|
+
Leerness v1.18.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
|
|
244
244
|
|
|
245
245
|
```jsonc
|
|
246
246
|
// 카테고리별
|
|
@@ -261,7 +261,7 @@ Leerness v1.16.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code
|
|
|
261
261
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
262
262
|
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) 다음 라운드 예약.
|
|
263
263
|
|
|
264
|
-
현재 누적: **70 라운드 (1.9.40 → 1.
|
|
264
|
+
현재 누적: **70 라운드 (1.9.40 → 1.18.0)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
265
265
|
|
|
266
266
|
### 성능 가이드 (1.9.140 측정)
|
|
267
267
|
|
|
@@ -299,6 +299,6 @@ leerness release pack --close --auto-main-push
|
|
|
299
299
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
300
300
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
301
301
|
|
|
302
|
-
Last synced by Leerness v1.
|
|
302
|
+
Last synced by Leerness v1.18.0: 2026-06-10
|
|
303
303
|
<!-- leerness:project-readme:end -->
|
|
304
304
|
|
package/bin/leerness.js
CHANGED
|
@@ -32,7 +32,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
|
|
|
32
32
|
// 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
|
|
33
33
|
const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_CATALOG, _TOOL_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 분리 · 1.11.4 (UR-0007): _TOOL_CATALOG
|
|
34
34
|
|
|
35
|
-
const VERSION = '1.
|
|
35
|
+
const VERSION = '1.18.0';
|
|
36
36
|
|
|
37
37
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
38
38
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -216,7 +216,7 @@ function _resolveRoot(positional) {
|
|
|
216
216
|
}
|
|
217
217
|
function nonFlagArgs() {
|
|
218
218
|
const out = [];
|
|
219
|
-
const withValue = new Set(['--language','--skills','--path','--status','--progress','--goal','--reason','--next','--target','--token-env','--package','--out','--from','--repo','--id','--note','--evidence','--query','--limit','--action','--agent','--tool','--doc','--command','--capability','--before','--after','--display','--threshold','--trigger','--check','--set','--min-score','--include','--days','--gh-pages-src','--roadmap','--since','--agents','--model','--timeout','--retry-on-fail','--label','--score','--tokens','--alternatives','--impact','--tag','--surface','--depends-on','--affects','--co-changes-with','--files','--branch','--remote','--task-add','--next-action','--role','--provider','--env-var','--deploy','--token-lifetime-hours','--port','--secret','--keep','--shell','--ps-version','--done-when']); // 1.14.2 (UR-0032): --done-when 값이 positional 로 누출돼 milestone 제목에 흡수되던 것
|
|
219
|
+
const withValue = new Set(['--language','--skills','--path','--status','--progress','--goal','--reason','--next','--target','--token-env','--package','--out','--from','--repo','--id','--note','--evidence','--query','--limit','--action','--agent','--tool','--doc','--command','--capability','--before','--after','--display','--threshold','--trigger','--check','--set','--min-score','--include','--days','--gh-pages-src','--roadmap','--since','--agents','--model','--timeout','--retry-on-fail','--label','--score','--tokens','--alternatives','--impact','--tag','--surface','--depends-on','--affects','--co-changes-with','--files','--branch','--remote','--task-add','--next-action','--role','--provider','--env-var','--deploy','--token-lifetime-hours','--port','--secret','--keep','--shell','--ps-version','--done-when','--test-cmd']); // 1.14.2 (UR-0032): --done-when 값이 positional 로 누출돼 milestone 제목에 흡수되던 것 차단. 1.17.2 (UR-0045): --test-cmd 동일 원칙(신규 value-flag 는 반드시 여기 등록)
|
|
220
220
|
const a = process.argv.slice(2);
|
|
221
221
|
for (let i = 0; i < a.length; i++) {
|
|
222
222
|
const x = a[i];
|
|
@@ -281,7 +281,7 @@ function managedReadmeBlock(project) {
|
|
|
281
281
|
'',
|
|
282
282
|
'Leerness 는 **실행기/코딩 에이전트가 아니라**, 어떤 AI 코딩 에이전트(Claude Code · Codex · Cursor · Goose 등) 위에도 얹는 **범용 운영 레이어**입니다. 5개 공통 계층을 제공합니다:',
|
|
283
283
|
'',
|
|
284
|
-
'- **기억(Memory)** — 프로젝트 상태/결정/진행을 `.
|
|
284
|
+
'- **기억(Memory)** — 프로젝트 상태/결정/진행을 `.harness/` 에 영속화',
|
|
285
285
|
'- **정책(Policy)** — 8단계 권한 등급 + enforce (read-only→publish), MCP 호출 게이트',
|
|
286
286
|
'- **인수인계(Handoff)** — 에이전트 간 컨텍스트 표준 전달 + `get_project_context` 1콜 온보딩',
|
|
287
287
|
'- **검증(Verification)** — 근거 기반 완료 검증으로 허위 완료 차단',
|
|
@@ -3545,6 +3545,67 @@ function _selfTestCases() {
|
|
|
3545
3545
|
const f2 = src.includes('_cellSafe(r.request)') && src.includes('_cellSafe(r.rule)');
|
|
3546
3546
|
return f1 && f2;
|
|
3547
3547
|
} },
|
|
3548
|
+
{ name: '외부클린룸 C2/C3/C4: gate --json 단일객체 + memory search --json + about .harness 정합 (1.16.1)', run: () => {
|
|
3549
|
+
const src = read(__filename);
|
|
3550
|
+
const c2 = src.includes("const jsonMode = has('--json'); // 외부리뷰 C2") && src.includes('ok: bad === 0, total: checks.length, failed: bad, checks');
|
|
3551
|
+
const c3 = src.includes('// 외부리뷰 C3: --json 일관성') && src.includes('JSON.stringify({ version: VERSION, query, total, includeCode');
|
|
3552
|
+
const _badDir = '.leern' + 'ess/ 에 영속화 (state start'; // 자기참조 회피: 분할 — about state 줄이 .leerness 로 남아있으면 감지
|
|
3553
|
+
const c4 = src.includes('상태/결정/진행을 .harness/ 에 영속화 (task/decision') && !src.includes('상태/결정/진행을 ' + _badDir);
|
|
3554
|
+
return c2 && c3 && c4;
|
|
3555
|
+
} },
|
|
3556
|
+
{ name: '외부클린룸 UR-0042: bare 명령그룹(rule/skill/feature/memory) → 사용법 힌트(unknown command 아님) (1.16.2)', run: () => {
|
|
3557
|
+
const src = read(__filename);
|
|
3558
|
+
return src.includes('const _GROUP_USAGE = {') && src.includes("if (_GROUP_USAGE[cmd] && !args[1])") && src.includes("'subcommand_required'");
|
|
3559
|
+
} },
|
|
3560
|
+
{ name: '17th 버그헌트 P2: plan add 공백제목 trim(기본값) + milestone 파서 개행 미흡수 (1.17.1)', run: () => {
|
|
3561
|
+
const src = read(__filename);
|
|
3562
|
+
const wired = src.includes("args.slice(2).join(' ').trim() || '새 계획'") && src.includes('(M-\\d{4})\\.[ \\t]*(.+?)$');
|
|
3563
|
+
// 파서 동작: 공백제목 milestone 이 다음 줄 'Status:' 를 제목으로 먹지 않음
|
|
3564
|
+
const block = '### M-0006. \nStatus: planned\nProgress: 0%\n';
|
|
3565
|
+
const m = block.match(/^### (M-\d{4})\.[ \t]*(.+?)$/m);
|
|
3566
|
+
const safe = !m || (m[2] || '').indexOf('Status') === -1;
|
|
3567
|
+
return wired && safe;
|
|
3568
|
+
} },
|
|
3569
|
+
{ name: '범용성 P1 (UR-0045): verify-claim --run-tests 테스트 명령 해석 체인(--test-cmd>config>실제 npm test>skip) + placeholder 미실행 (1.17.2)', run: () => {
|
|
3570
|
+
const src = read(__filename);
|
|
3571
|
+
const chain = src.includes("let testCmd = arg('--test-cmd', null)") && src.includes("typeof cfg.testCommand === 'string'") && src.includes("!/no test specified/i.test(ts)") && src.includes('테스트 명령 미지정');
|
|
3572
|
+
const wired = src.includes("'--done-when','--test-cmd'") && src.includes('runCommandSafe(testCmd, []') && src.includes('cmd: testCmd');
|
|
3573
|
+
// pytest 출력 파싱: "3 passed in 0.05s"
|
|
3574
|
+
const py = ('3 passed in 0.05s'.match(/(\d+)\s+passed\b/i) || [])[1] === '3';
|
|
3575
|
+
return chain && wired && py;
|
|
3576
|
+
} },
|
|
3577
|
+
{ name: '범용성 P1② (UR-0046): verify-claim 스텁 구현 차단 + 테스트-구현 연결 검사 (1.17.3)', run: () => {
|
|
3578
|
+
const src = read(__filename);
|
|
3579
|
+
const wired = src.includes('const stubFiles = [];') && src.includes('implementationSubstance: stubFiles.length === 0') && src.includes('testImplLink: testLinkOk') && src.includes("(claimsChecked && stubFiles.length > 0) || (has('--strict-claims') && testLinkOk === false)");
|
|
3580
|
+
// 스텁 판정 로직 재현: 주석뿐 파일 → 코드줄 0
|
|
3581
|
+
const stub = '// TODO: call stripe\n// TODO: verify signature\n\n';
|
|
3582
|
+
const real = '// handler\nmodule.exports = function(){ return 1; };\n';
|
|
3583
|
+
const count = (s) => s.replace(/\/\*[\s\S]*?\*\//g, '').split('\n').map(l => l.trim()).filter(t => t && !t.startsWith('//') && !t.startsWith('#')).length;
|
|
3584
|
+
return wired && count(stub) === 0 && count(real) > 0;
|
|
3585
|
+
} },
|
|
3586
|
+
{ name: '범용성 P2 (UR-0047): 테스트 카운트 관례 확대(pytest/루트 *.test.*) + 측정불가=검증미수행(역전 해소) (1.17.4)', run: () => {
|
|
3587
|
+
const src = read(__filename);
|
|
3588
|
+
const wired = src.includes('const _countTests = (fp)') && src.includes("def\\s+test_") && src.includes('측정 불가 — 주장') && src.includes('out.verdict.testCountMatch === false') && src.includes('const testMeasured = actualTestCount != null;');
|
|
3589
|
+
// 관례 글롭: pytest/루트 test 파일명 매칭
|
|
3590
|
+
const re = /^test_.+\.py$|_test\.py$|\.test\.[cm]?[jt]sx?$|\.spec\.[cm]?[jt]sx?$/i;
|
|
3591
|
+
const glob = re.test('test_calc.py') && re.test('calc_test.py') && re.test('rateLimiter.test.js') && re.test('app.spec.ts') && !re.test('calc.py') && !re.test('index.js');
|
|
3592
|
+
// 파이썬 카운트: def test_ 2개
|
|
3593
|
+
const py = ('def test_a():\n pass\ndef test_b():\n pass\n'.match(/^\s*def\s+test_/gm) || []).length === 2;
|
|
3594
|
+
return wired && glob && py;
|
|
3595
|
+
} },
|
|
3596
|
+
{ name: '범용성 P2 (UR-0048): task update unknown flag 거부 + did-you-mean(인수인계 유실 차단) (1.17.5)', run: () => {
|
|
3597
|
+
const src = read(__filename);
|
|
3598
|
+
const wired = src.includes('function _rejectUnknownFlags(allowed, usageHint)') && src.includes("'unknown_flag'") && src.includes("_rejectUnknownFlags(['--status', '--evidence', '--next', '--note']");
|
|
3599
|
+
// did-you-mean prefix 로직: --next-action 은 --next 를 prefix 로 가짐
|
|
3600
|
+
const dymOk = '--next-action'.startsWith('--next');
|
|
3601
|
+
return wired && dymOk;
|
|
3602
|
+
} },
|
|
3603
|
+
{ name: '범용성 P2 완결 (UR-0049): session close 마감 정합 — done 낙관 재확인 + 시크릿 재확인 (1.17.6)', run: () => {
|
|
3604
|
+
const sc = read(path.join(path.dirname(__filename), '..', 'lib', 'session-close.js'));
|
|
3605
|
+
const wired = sc.includes('_detectOptimism, _scanCodeForPatterns, _collectSecretFindings } = deps') && sc.includes('optimismUnresolved') && sc.includes('jsonResult.closeSecurity') && sc.includes('마감 보안: 커밋 대상 시크릿');
|
|
3606
|
+
const injected = read(__filename).includes('_updateUserRequest, _detectOptimism, _scanCodeForPatterns, _collectSecretFindings });');
|
|
3607
|
+
return wired && injected;
|
|
3608
|
+
} },
|
|
3548
3609
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
3549
3610
|
];
|
|
3550
3611
|
}
|
|
@@ -4109,7 +4170,7 @@ function commandsCmd(root) {
|
|
|
4109
4170
|
{ cmd: 'scan secrets [path]', desc: '시크릿 탐지' },
|
|
4110
4171
|
{ cmd: 'encoding check [path]', desc: '인코딩 검증' },
|
|
4111
4172
|
{ cmd: 'lazy detect [path] [--json]', desc: '게으른 작업 감지 (1.9.101)' },
|
|
4112
|
-
{ cmd: 'verify-claim <T-ID> [--run-tests] [--strict-claims] [--require-evidence]', desc: '주장 검증 (1.9.18~26) — --require-evidence: done 주장에 파일+테스트 근거 강제 (1.9.287)' },
|
|
4173
|
+
{ cmd: 'verify-claim <T-ID> [--run-tests] [--test-cmd "<명령>"] [--strict-claims] [--require-evidence]', desc: '주장 검증 (1.9.18~26) — --require-evidence: done 주장에 파일+테스트 근거 강제 (1.9.287) · --test-cmd: 비-JS 테스트 명령 (1.17.2)' },
|
|
4113
4174
|
{ cmd: 'optimism-check <T-ID>', desc: '낙관적 API 감지 (1.9.26)' },
|
|
4114
4175
|
{ cmd: 'requests audit|list|complete|drop|auto-complete', desc: '사용자 요청 추적 (1.9.207/223)' },
|
|
4115
4176
|
{ cmd: 'pre-wake-audit [path] [--last]', desc: 'sleep 전 점검 (1.9.209)' },
|
|
@@ -6433,7 +6494,7 @@ function planListCmd(root, opts = {}) {
|
|
|
6433
6494
|
// ### M-XXXX. <title> 블록 추출
|
|
6434
6495
|
const blocks = text.split(/\n(?=### M-\d{4}\.)/);
|
|
6435
6496
|
for (const b of blocks) {
|
|
6436
|
-
const headerMatch = b.match(/^### (M-\d{4})
|
|
6497
|
+
const headerMatch = b.match(/^### (M-\d{4})\.[ \t]*(.+?)$/m);
|
|
6437
6498
|
if (!headerMatch) continue;
|
|
6438
6499
|
const id = headerMatch[1];
|
|
6439
6500
|
const title = headerMatch[2].trim();
|
|
@@ -6703,8 +6764,22 @@ function taskAdd(root, text) {
|
|
|
6703
6764
|
} catch {} // review 실패는 task add 자체에 영향 X
|
|
6704
6765
|
}
|
|
6705
6766
|
}
|
|
6767
|
+
// 1.17.5 (UR-0048, 5축 실증 P2): 모르는 옵션 조용히 무시 차단 — `task update --next-action "x"` 처럼 오타/미존재 플래그가
|
|
6768
|
+
// "✓ task updated" 와 함께 값을 버려, 쓴 에이전트는 기록됐다고 믿고 다음 에이전트는 placeholder 를 받는(인수인계 유실) 최악의 실패 양식.
|
|
6769
|
+
// prefix 기반 did-you-mean(--next-action → --next) + exit 1. 전역 플래그(--path/--json/--force/--lenient)는 항상 허용.
|
|
6770
|
+
function _rejectUnknownFlags(allowed, usageHint) {
|
|
6771
|
+
const all = new Set([...allowed, '--path', '--json', '--force', '--lenient']);
|
|
6772
|
+
const seen = process.argv.slice(2).filter(a => a.startsWith('--')).map(a => a.split('=')[0]);
|
|
6773
|
+
const unknown = [...new Set(seen.filter(f => !all.has(f)))];
|
|
6774
|
+
if (!unknown.length) return true;
|
|
6775
|
+
const dym = (u) => { const c = [...all].find(k => u.startsWith(k) || k.startsWith(u)); return c ? ` — 혹시 ${c}?` : ''; };
|
|
6776
|
+
failJson(has('--json'), 'unknown_flag', `알 수 없는 옵션: ${unknown.map(u => u + dym(u)).join(', ')} (지원: ${[...allowed].join(' ')}${usageHint ? ' · ' + usageHint : ''}) — 값이 조용히 버려지는 것을 방지하기 위해 거부`);
|
|
6777
|
+
return false;
|
|
6778
|
+
}
|
|
6779
|
+
|
|
6706
6780
|
function taskUpdate(root, id) {
|
|
6707
6781
|
if (!_requireInit(root, 'task update')) return; // 1.9.311 (UR-0047): init 가드
|
|
6782
|
+
if (!_rejectUnknownFlags(['--status', '--evidence', '--next', '--note'], 'task update T-0001 --status done --evidence "..." --next "..."')) { process.exitCode = 1; return; } // 1.17.5 (UR-0048)
|
|
6708
6783
|
if (!id) return fail('id required (e.g., task update T-0001 --status in-progress)');
|
|
6709
6784
|
if (!_validateChoice(arg('--status', null), TASK_STATUSES, 'task status')) { process.exitCode = 1; return; } // 1.9.310 (UR-0046)
|
|
6710
6785
|
const rows = readProgressRows(root);
|
|
@@ -7135,7 +7210,7 @@ function _jaccard(a, b) {
|
|
|
7135
7210
|
function taskRelink(root) {
|
|
7136
7211
|
root = absRoot(root);
|
|
7137
7212
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
7138
|
-
const milestones = [...planText.matchAll(/^### (M-\d{4})
|
|
7213
|
+
const milestones = [...planText.matchAll(/^### (M-\d{4})\.[ \t]*(.+?)$/gm)]
|
|
7139
7214
|
.map(m => ({ id: m[1], text: m[2].trim() }));
|
|
7140
7215
|
const rows = readProgressRows(root);
|
|
7141
7216
|
const linkedM = new Set(rows.map(r => (r.evidence.match(/M-\d{4}/) || [])[0]).filter(Boolean));
|
|
@@ -7636,7 +7711,8 @@ function preCheck(root) {
|
|
|
7636
7711
|
|
|
7637
7712
|
function memorySearch(root, query) {
|
|
7638
7713
|
root = absRoot(root);
|
|
7639
|
-
|
|
7714
|
+
const jsonMode = has('--json'); const results = []; // 외부리뷰 C3: --json 일관성(이전엔 --json 무시하고 텍스트만)
|
|
7715
|
+
if (!query) { failJson(jsonMode, 'query_required', 'query required (e.g., memory search "키워드")'); return; }
|
|
7640
7716
|
// 1.13.1 (15th 블라인드 리뷰 P1, Sonnet): lessons.md + rules.md 누락 수정 — memory search 가 5종 메모리 표면을 표방하나 lesson/rule 을 검색 못 해(lesson add/rule add 로 저장한 교훈·룰이 'no matches') 모순감지 핵심 용도가 훼손됐음.
|
|
7641
7717
|
const files = ['.harness/decisions.md','.harness/lessons.md','.harness/rules.md','.harness/task-log.md','.harness/session-handoff.md','.harness/progress-tracker.md','.harness/plan.md','.harness/review-evidence.md','.harness/architecture.md'];
|
|
7642
7718
|
const re = new RegExp(query.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'), 'i');
|
|
@@ -7646,8 +7722,8 @@ function memorySearch(root, query) {
|
|
|
7646
7722
|
const lines = read(p).split('\n');
|
|
7647
7723
|
const hits = lines.map((line, i) => ({ line, i })).filter(x => re.test(x.line));
|
|
7648
7724
|
if (hits.length) {
|
|
7649
|
-
log(`\n# ${f}`);
|
|
7650
|
-
for (const h of hits.slice(0, _parseLimit(arg('--limit','5'),5))) log(` L${h.i+1}: ${h.line.trim()}`);
|
|
7725
|
+
if (!jsonMode) log(`\n# ${f}`);
|
|
7726
|
+
for (const h of hits.slice(0, _parseLimit(arg('--limit','5'),5))) { if (!jsonMode) log(` L${h.i+1}: ${h.line.trim()}`); results.push({ file: f, line: h.i + 1, text: h.line.trim() }); }
|
|
7651
7727
|
total += hits.length;
|
|
7652
7728
|
}
|
|
7653
7729
|
}
|
|
@@ -7668,8 +7744,8 @@ function memorySearch(root, query) {
|
|
|
7668
7744
|
const lines = txt.split('\n');
|
|
7669
7745
|
const hits = lines.map((line, i) => ({ line, i })).filter(x => re.test(x.line));
|
|
7670
7746
|
if (hits.length) {
|
|
7671
|
-
log(`\n# ${rel(root, p)}`);
|
|
7672
|
-
for (const h of hits.slice(0, _parseLimit(arg('--limit','5'),5))) log(` L${h.i+1}: ${h.line.trim().slice(0, 160)}`);
|
|
7747
|
+
if (!jsonMode) log(`\n# ${rel(root, p)}`);
|
|
7748
|
+
for (const h of hits.slice(0, _parseLimit(arg('--limit','5'),5))) { if (!jsonMode) log(` L${h.i+1}: ${h.line.trim().slice(0, 160)}`); results.push({ file: rel(root, p), line: h.i + 1, text: h.line.trim().slice(0, 160) }); }
|
|
7673
7749
|
total += hits.length;
|
|
7674
7750
|
}
|
|
7675
7751
|
}
|
|
@@ -7677,6 +7753,7 @@ function memorySearch(root, query) {
|
|
|
7677
7753
|
walkCodeDir(dp);
|
|
7678
7754
|
}
|
|
7679
7755
|
}
|
|
7756
|
+
if (jsonMode) { log(JSON.stringify({ version: VERSION, query, total, includeCode: has('--include-code'), results }, null, 2)); return; }
|
|
7680
7757
|
if (total === 0) log('(no matches)');
|
|
7681
7758
|
else log(`\n${total} matches${has('--include-code') ? ' (소스 코드 포함)' : ''}`);
|
|
7682
7759
|
}
|
|
@@ -9598,6 +9675,11 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9598
9675
|
// 4) N개 테스트 (단순 카운트)
|
|
9599
9676
|
const m4 = evidence.match(/(\d+)\s*개\s*테스트/);
|
|
9600
9677
|
if (m4) declaredTestCount = parseInt(m4[1], 10);
|
|
9678
|
+
// 4b) 테스트 N개 (1.17.4 UR-0047: 한국어 자연어순 '테스트 50개 통과' — 이전 미인식으로 부풀린 주장이 카운트 검증을 아예 안 탔음)
|
|
9679
|
+
if (!declaredTestCount) {
|
|
9680
|
+
const m4b = evidence.match(/테스트\s*(\d+)\s*개/);
|
|
9681
|
+
if (m4b) declaredTestCount = parseInt(m4b[1], 10);
|
|
9682
|
+
}
|
|
9601
9683
|
// 5) N tests (영문 단순 카운트)
|
|
9602
9684
|
if (!declaredTestCount) {
|
|
9603
9685
|
const m5 = evidence.match(/(\d+)\s*tests?\b/i);
|
|
@@ -9606,6 +9688,27 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9606
9688
|
|
|
9607
9689
|
// 실제 파일 존재 검사
|
|
9608
9690
|
const fileChecks = files.map(f => ({ file: f, exists: exists(path.join(root, f)) }));
|
|
9691
|
+
|
|
9692
|
+
// 1.17.3 (UR-0046 범용성 P1②): 빈껍데기(스텁) 구현 + 테스트-구현 연결 검사 — "주석뿐 구현 + assert(true) 테스트"가 verify-claim 을 exit 0 으로 통과하던 공격(5축 실증 Attack C) 차단.
|
|
9693
|
+
// ① 스텁: 주장된 코드 파일(테스트 제외)의 비주석 코드줄이 0 이면 확정 스텁 — done 게이팅 FAIL(확실 신호만, 과탐 0).
|
|
9694
|
+
// ② 연결: 주장에 구현+테스트가 모두 있는데 어떤 테스트도 구현 파일명(basename)을 참조하지 않으면 — 기본 advisory ⚠, --strict-claims 시 FAIL.
|
|
9695
|
+
const _VC_CODE_EXT = /\.(js|mjs|cjs|jsx|ts|tsx|py|rb|go|rs|java|cs|php)$/i;
|
|
9696
|
+
const _VC_TEST_PAT = /(^|[\\/])(test_[^\\/]+\.[a-z]+|[^\\/]+[._-]test\.[a-z]+|[^\\/]+\.spec\.[a-z]+)$|(^|[\\/])tests?[\\/]/i;
|
|
9697
|
+
const stubFiles = [];
|
|
9698
|
+
for (const c of fileChecks) {
|
|
9699
|
+
if (!c.exists || !_VC_CODE_EXT.test(c.file) || _VC_TEST_PAT.test(c.file)) continue;
|
|
9700
|
+
let body = ''; try { body = read(path.join(root, c.file)); } catch { continue; }
|
|
9701
|
+
if (!body || body.length > 512 * 1024) continue;
|
|
9702
|
+
const codeLines = body.replace(/\/\*[\s\S]*?\*\//g, '').split('\n').map(l => l.trim()).filter(t => t && !t.startsWith('//') && !t.startsWith('#'));
|
|
9703
|
+
if (codeLines.length === 0) stubFiles.push(c.file);
|
|
9704
|
+
}
|
|
9705
|
+
const _vcImpl = fileChecks.filter(c => c.exists && _VC_CODE_EXT.test(c.file) && !_VC_TEST_PAT.test(c.file)).map(c => c.file);
|
|
9706
|
+
const _vcTests = fileChecks.filter(c => c.exists && _VC_CODE_EXT.test(c.file) && _VC_TEST_PAT.test(c.file)).map(c => c.file);
|
|
9707
|
+
let testLinkOk = null; // null = 판단 불가(구현·테스트가 함께 주장되지 않음)
|
|
9708
|
+
if (_vcImpl.length && _vcTests.length) {
|
|
9709
|
+
const bases = _vcImpl.map(f => path.basename(f).replace(/\.[a-z]+$/i, ''));
|
|
9710
|
+
testLinkOk = _vcTests.some(tf => { let t = ''; try { t = read(path.join(root, tf)); } catch { return false; } return bases.some(b => b && t.includes(b)); });
|
|
9711
|
+
}
|
|
9609
9712
|
// 1.9.302 (UR-0042, 외부리뷰 Opus G-1): git diff 시맨틱 교차검증 — 주장한 파일이 실제로 변경됐는가.
|
|
9610
9713
|
// "파일 존재"만으로는 "테스트만 통과하면 done" 허위완료를 못 막음(Opus). git working tree+직전커밋 변경과 대조.
|
|
9611
9714
|
const gitChanged = _gitChangedFiles(root); // Set | null(git repo 아님 → 검증 불가)
|
|
@@ -9615,33 +9718,55 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9615
9718
|
// 1.13.2 (Karpathy 가이드라인 3 "외과적 변경", UR-0030): 역방향 교차검증 — git 에 변경됐으나 evidence/주장에 없는 파일(scope-creep / 요청 범위 밖 변경 신호). 하네스 자체 기록(.harness 등)은 제외. advisory(오탐 방지 — 기본 FAIL 아님, 표면화만).
|
|
9616
9719
|
const _SCOPE_SKIP = /^(\.harness[\\/]|\.git[\\/]|node_modules[\\/]|\.claude[\\/]|dist[\\/]|build[\\/])/;
|
|
9617
9720
|
const changedNotClaimed = gitApplicable ? [...gitChanged].filter(g => !_SCOPE_SKIP.test(g) && !files.some(f => _claimFileInGit(f, new Set([g])))) : [];
|
|
9618
|
-
// 테스트
|
|
9721
|
+
// 테스트 카운트 (1.17.4, UR-0047): 주장된 테스트 파일 우선 + 관례 글롭(pytest test_*.py·*_test.py / 루트·tests/ 의 *.test.*·*.spec.*) 인식.
|
|
9722
|
+
// 이전엔 tests/test.js 등 3개 하드코딩 — pytest/node:test 루트 관례가 안 보여 "파일 못 찾음"인데 ✓ pass 로 표기(측정실패=통과 역전, 5축 실증 P2 공통).
|
|
9723
|
+
const _countTests = (fp) => {
|
|
9724
|
+
let t = ''; try { t = read(fp); } catch { return 0; }
|
|
9725
|
+
if (/\.py$/i.test(fp)) { const d = (t.match(/^\s*def\s+test_/gm) || []).length; return d || (t.match(/^\s*assert\b/gm) || []).length; }
|
|
9726
|
+
return (t.match(/\bcheck\s*\(/g) || t.match(/\b(it|test)\s*\(/g) || []).length;
|
|
9727
|
+
};
|
|
9619
9728
|
let actualTestCount = null;
|
|
9620
|
-
|
|
9621
|
-
|
|
9622
|
-
|
|
9623
|
-
|
|
9624
|
-
|
|
9625
|
-
|
|
9626
|
-
|
|
9729
|
+
if (_vcTests.length) {
|
|
9730
|
+
actualTestCount = _vcTests.reduce((a, f) => a + _countTests(path.join(root, f)), 0);
|
|
9731
|
+
} else {
|
|
9732
|
+
const found = new Set();
|
|
9733
|
+
for (const tf of ['tests/test.js', 'test/test.js', 'tests/index.js']) if (exists(path.join(root, tf))) found.add(tf);
|
|
9734
|
+
if (!found.size) {
|
|
9735
|
+
for (const dir of ['', 'tests', 'test']) {
|
|
9736
|
+
let ents = []; try { ents = fs.readdirSync(path.join(root, dir)); } catch { continue; }
|
|
9737
|
+
for (const e of ents) if (/^test_.+\.py$|_test\.py$|\.test\.[cm]?[jt]sx?$|\.spec\.[cm]?[jt]sx?$/i.test(e)) found.add(dir ? dir + '/' + e : e);
|
|
9738
|
+
if (found.size) break; // 루트 우선 (루트에 있으면 tests/ 중복 스캔 안 함)
|
|
9739
|
+
}
|
|
9627
9740
|
}
|
|
9741
|
+
if (found.size) actualTestCount = [...found].reduce((a, f) => a + _countTests(path.join(root, f)), 0);
|
|
9628
9742
|
}
|
|
9743
|
+
const testMeasured = actualTestCount != null;
|
|
9629
9744
|
|
|
9630
|
-
// 1.9.19: --run-tests —
|
|
9745
|
+
// 1.9.19: --run-tests — 테스트 자동 실행 + pass/fail 파싱
|
|
9746
|
+
// 1.17.2 (UR-0045 범용성 P1): 테스트 명령 해석 체인 — --test-cmd > leerness-config.json testCommand > 실제 npm test 스크립트 > skip.
|
|
9747
|
+
// 이전엔 npm test 하드코딩 → 비-JS(파이썬 등) 프로젝트에서 npm init 잔재 placeholder("no test specified"&&exit 1)가 실행돼
|
|
9748
|
+
// 테스트 전부 통과한 작업을 "주장 불일치 FAIL"로 오판(5축 클린룸 실증 P1). placeholder 는 테스트가 아니므로 skip 처리.
|
|
9631
9749
|
let runResult = null;
|
|
9632
9750
|
if (has('--run-tests')) {
|
|
9633
|
-
|
|
9634
|
-
if (!
|
|
9635
|
-
|
|
9751
|
+
let testCmd = arg('--test-cmd', null);
|
|
9752
|
+
if (!testCmd) {
|
|
9753
|
+
try { const cfg = JSON.parse(read(path.join(root, '.harness', 'leerness-config.json'))); if (cfg && typeof cfg.testCommand === 'string' && cfg.testCommand.trim()) testCmd = cfg.testCommand.trim(); } catch {}
|
|
9754
|
+
}
|
|
9755
|
+
if (!testCmd) {
|
|
9756
|
+
const pkgPath = path.join(root, 'package.json');
|
|
9757
|
+
if (exists(pkgPath)) {
|
|
9758
|
+
let pkg = null;
|
|
9759
|
+
try { pkg = JSON.parse(read(pkgPath)); } catch {}
|
|
9760
|
+
const ts = pkg && pkg.scripts && pkg.scripts.test;
|
|
9761
|
+
if (ts && !/no test specified/i.test(ts)) testCmd = 'npm test';
|
|
9762
|
+
}
|
|
9763
|
+
}
|
|
9764
|
+
if (!testCmd) {
|
|
9765
|
+
runResult = { skipped: true, reason: '테스트 명령 미지정 — 비-JS 프로젝트는 --test-cmd "<명령>" 또는 .harness/leerness-config.json 의 "testCommand" 로 지정 (불일치 판정 아님)' };
|
|
9636
9766
|
} else {
|
|
9637
|
-
|
|
9638
|
-
|
|
9639
|
-
|
|
9640
|
-
if (!hasTestScript) {
|
|
9641
|
-
runResult = { skipped: true, reason: 'scripts.test 없음' };
|
|
9642
|
-
} else {
|
|
9643
|
-
// 1.9.299 (UR-0039): 신뢰 못 할 워크스페이스 npm test → runCommandSafe + scrubSecrets (시크릿 노출 차단 + cwd jail).
|
|
9644
|
-
const r = runCommandSafe('npm test', [], { cwd: root, root, encoding: 'utf8', allowShell: true, scrubSecrets: true, timeout: 5 * 60 * 1000, kind: 'verify_claim_test' });
|
|
9767
|
+
{
|
|
9768
|
+
// 1.9.299 (UR-0039): 신뢰 못 할 워크스페이스 테스트 실행 → runCommandSafe + scrubSecrets (시크릿 노출 차단 + cwd jail).
|
|
9769
|
+
const r = runCommandSafe(testCmd, [], { cwd: root, root, encoding: 'utf8', allowShell: true, scrubSecrets: true, timeout: 5 * 60 * 1000, kind: 'verify_claim_test' });
|
|
9645
9770
|
const out = (r.stdout || '') + (r.stderr || '');
|
|
9646
9771
|
// 1.9.20: 파싱 패턴 확장 — 한국어 + jest/mocha/tap/vitest
|
|
9647
9772
|
let parsed = null;
|
|
@@ -9663,8 +9788,14 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9663
9788
|
const m4 = out.match(/#\s*pass\s+(\d+)/i);
|
|
9664
9789
|
if (m4) parsed = { num: parseInt(m4[1], 10), denom: parseInt(m4[1], 10) };
|
|
9665
9790
|
}
|
|
9791
|
+
// 5) pytest: "N passed in 0.12s" (UR-0045 — 파이썬 러너 출력 인식)
|
|
9792
|
+
if (!parsed) {
|
|
9793
|
+
const m5 = out.match(/(\d+)\s+passed\b/i);
|
|
9794
|
+
if (m5) parsed = { num: parseInt(m5[1], 10), denom: parseInt(m5[1], 10) };
|
|
9795
|
+
}
|
|
9666
9796
|
runResult = {
|
|
9667
9797
|
skipped: false,
|
|
9798
|
+
cmd: testCmd,
|
|
9668
9799
|
exitCode: r.status,
|
|
9669
9800
|
parsed,
|
|
9670
9801
|
allPassed: r.status === 0 && (!parsed || (parsed && parsed.num === parsed.denom))
|
|
@@ -9709,11 +9840,14 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9709
9840
|
actual: { fileChecks, testCount: actualTestCount },
|
|
9710
9841
|
verdict: {
|
|
9711
9842
|
filesAllExist,
|
|
9712
|
-
testCountMatch: declaredTestCount == null
|
|
9843
|
+
testCountMatch: declaredTestCount == null ? null : (!testMeasured ? null : actualTestCount >= declaredTestCount), // 1.17.4 (UR-0047): null=측정불가(검증 미수행 — pass 아님), false 만 게이팅
|
|
9713
9844
|
evidenceComplete: !mustHaveEvidence ? null : evq.ok,
|
|
9714
9845
|
claimsConsistent: !claimsChecked ? null : strictOk, // 1.11.2 (UR-0175): optimism+정직성 (기본 게이팅)
|
|
9715
|
-
gitCrossCheck: !gitApplicable ? null : !gitStrongMismatch
|
|
9846
|
+
gitCrossCheck: !gitApplicable ? null : !gitStrongMismatch, // 1.11.2 (UR-0175): git 교차검증 (머신 경로 노출)
|
|
9847
|
+
implementationSubstance: stubFiles.length === 0, // 1.17.3 (UR-0046): 주장된 구현이 주석/빈껍데기뿐이면 false
|
|
9848
|
+
testImplLink: testLinkOk // 1.17.3 (UR-0046): 테스트가 구현을 참조하는가 (null=판단불가)
|
|
9716
9849
|
},
|
|
9850
|
+
stubFiles: stubFiles.slice(0, 10),
|
|
9717
9851
|
evidence: { required: mustHaveEvidence, ...evq },
|
|
9718
9852
|
claims: !claimsChecked ? null : { ok: strictOk, optimism: optimismSuspects.map(s => ({ kind: s.kind, label: s.label })), honesty: honestyFindings.map(f => ({ dim: f.dim, label: f.label })) },
|
|
9719
9853
|
git: gitChanged === null ? { applicable: false, reason: 'not-a-git-repo' } : (!gitApplicable ? { applicable: false, reason: 'no-working-changes-or-no-claimed-files' } : { applicable: true, claimedInGit: claimedInGit.length, claimedNotInGit, strongMismatch: gitStrongMismatch, changedNotClaimed }),
|
|
@@ -9730,7 +9864,7 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9730
9864
|
log(JSON.stringify(out, null, 2));
|
|
9731
9865
|
if (runResult && !runResult.skipped && !runResult.allPassed) return process.exit(1);
|
|
9732
9866
|
// 1.11.2 (UR-0175): --json 도 optimism+git 게이팅 — 머신 경로가 허위완료를 통과시키지 않도록(human 경로와 동일).
|
|
9733
|
-
if (!filesAllExist ||
|
|
9867
|
+
if (!filesAllExist || out.verdict.testCountMatch === false || !evidenceQualityOk || (claimsChecked && !strictOk) || !gitClaimOk || (claimsChecked && stubFiles.length > 0) || (has('--strict-claims') && testLinkOk === false)) return process.exit(1); // 1.17.3 (UR-0046): 스텁(done 기본) + 테스트 미연결(strict). 1.17.4 (UR-0047): testCountMatch null(측정불가)은 미기여
|
|
9734
9868
|
return;
|
|
9735
9869
|
}
|
|
9736
9870
|
|
|
@@ -9748,15 +9882,15 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9748
9882
|
log(`## 🧪 테스트 카운트`);
|
|
9749
9883
|
if (declaredPass) log(` 주장 (pass): ${declaredPass.num}/${declaredPass.denom}`);
|
|
9750
9884
|
if (declaredTestCount) log(` 주장 (개수): ${declaredTestCount}개`);
|
|
9751
|
-
if (actualTestCount != null) log(` 실측:
|
|
9752
|
-
else log(` 실측: 테스트 파일 못 찾음 (
|
|
9885
|
+
if (actualTestCount != null) log(` 실측: ${actualTestCount}개 테스트 호출 (${_vcTests.length ? '주장된 테스트 파일' : '관례 탐색: 루트/tests·test_*.py·*.test.*'})`);
|
|
9886
|
+
else log(` 실측: 테스트 파일 못 찾음 — 카운트 검증 미수행 (pass 아님)`);
|
|
9753
9887
|
|
|
9754
9888
|
// 1.9.19: --run-tests 결과
|
|
9755
9889
|
let runTestsOk = true;
|
|
9756
9890
|
let declaredPassMatchesActual = true;
|
|
9757
9891
|
if (runResult) {
|
|
9758
9892
|
log('');
|
|
9759
|
-
log(`## 🚦
|
|
9893
|
+
log(`## 🚦 ${runResult.cmd || '테스트'} 실행 (--run-tests)`);
|
|
9760
9894
|
if (runResult.skipped) {
|
|
9761
9895
|
log(` ⚠ skipped: ${runResult.reason}`);
|
|
9762
9896
|
} else {
|
|
@@ -9778,7 +9912,8 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9778
9912
|
const testOk = declaredTestCount == null || actualTestCount == null || actualTestCount >= declaredTestCount;
|
|
9779
9913
|
log(`## 종합`);
|
|
9780
9914
|
log(` - 파일 모두 존재: ${allFilesOk ? '✓ pass' : '✗ FAIL (일부 누락)'}`);
|
|
9781
|
-
|
|
9915
|
+
// 1.17.4 (UR-0047): 측정 불가는 '통과' 가 아니라 '검증 미수행' — 이전엔 실측 0 인데 ✓ pass(실측≥주장) 모순 표기.
|
|
9916
|
+
log(` - 테스트 카운트: ${declaredTestCount == null ? '⊘ (주장 없음)' : !testMeasured ? `⊘ 측정 불가 — 주장 ${declaredTestCount}개 검증 미수행 (pass 아님)` : testOk ? '✓ pass (실측 ≥ 주장)' : '⚠ 주장보다 적음'}`);
|
|
9782
9917
|
if (runResult && !runResult.skipped) {
|
|
9783
9918
|
log(` - npm test 실행: ${runTestsOk ? '✓ all passed' : '✗ FAIL'}`);
|
|
9784
9919
|
if (declaredPass) log(` - 주장과 실행 결과 일치: ${declaredPassMatchesActual ? '✓ pass' : '⚠ 다름'}`);
|
|
@@ -9808,7 +9943,18 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9808
9943
|
log(` - evidence 완전성 (done 기본 강제): ${evidenceQualityOk ? '✓ pass (파일+테스트 근거 있음)' : `✗ FAIL (누락: ${evq.missing.join(', ')})`}`);
|
|
9809
9944
|
if (!evidenceQualityOk) log(` · done 주장은 수정 파일 경로 + 테스트명/개수 가 evidence 에 있어야 함 (테스트 통과만으로는 불충분). 완화: --lenient`);
|
|
9810
9945
|
}
|
|
9811
|
-
|
|
9946
|
+
// 1.17.3 (UR-0046): 구현 실체(스텁) + 테스트-구현 연결 — Attack C(주석뿐 구현+assert(true)) 차단.
|
|
9947
|
+
if (stubFiles.length) {
|
|
9948
|
+
log(` - 구현 실체 (done 기본): ✗ FAIL — 주장된 구현 파일이 주석/빈껍데기뿐: ${stubFiles.slice(0, 5).join(', ')} (비주석 코드 0줄)`);
|
|
9949
|
+
} else if (claimsChecked && _vcImpl.length) {
|
|
9950
|
+
log(` - 구현 실체 (done 기본): ✓ pass (주장 구현 파일에 실코드 존재)`);
|
|
9951
|
+
}
|
|
9952
|
+
if (testLinkOk === false) {
|
|
9953
|
+
log(` - 테스트-구현 연결: ⚠ 주장된 테스트(${_vcTests.slice(0, 3).join(', ')})가 구현 파일을 참조하지 않음 — 빈 테스트(assert(true)) 의심${has('--strict-claims') ? ' → FAIL' : ' (advisory — --strict-claims 시 FAIL)'}`);
|
|
9954
|
+
} else if (testLinkOk === true && claimsChecked) {
|
|
9955
|
+
log(` - 테스트-구현 연결: ✓ pass (테스트가 구현을 참조)`);
|
|
9956
|
+
}
|
|
9957
|
+
const overallFail = !allFilesOk || !testOk || (runResult && !runResult.skipped && !runTestsOk) || (claimsChecked && !strictOk) || !evidenceQualityOk || !gitClaimOk || (claimsChecked && stubFiles.length > 0) || (has('--strict-claims') && testLinkOk === false);
|
|
9812
9958
|
// 1.9.287: 정직한 한계 고지 — 테스트 통과 ≠ 의미적 구현 정확성
|
|
9813
9959
|
if (claimsChecked || mustHaveEvidence) {
|
|
9814
9960
|
log('');
|
|
@@ -11680,7 +11826,7 @@ function llmBenchRecordCmd(root) {
|
|
|
11680
11826
|
|
|
11681
11827
|
const _sessionClose = require('../lib/session-close');
|
|
11682
11828
|
// 1.9.425 (UR-0025/UR-0125 큰 핸들러 모듈화 10번째): sessionClose → lib/session-close.js (DI 위임)
|
|
11683
|
-
function sessionClose(root, opts = {}) { return _sessionClose.sessionClose(root, opts, { VERSION, STATUSES, MARK, has, arg, harnessPath: __filename, readProgressRows, evidencePath, handoffPath, currentStatePath, taskLogPath, verifyRules, _autoRoadmap, _readUsageStats, readSessionCounter, writeSessionCounter, _retroAggregate, _retroOneLine, retroCmd, _loadDecisions, readRules, planPath, _loadLessons, _readFeatureGraph, _auditUserRequests, _detectDeliveredRequests, _computeRoundHistory, _computeMilestones, _computeRecentChanges, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _scanShellScriptsEncoding, _listAPISkills, _matchAPISkills, _loadShellFailures, _shellEnvDrift, _runPreWakeAudit, _saveAndAppendPreWakeReport, _runIdempotencyAudit, _detectAbnormalShutdown, _updateUserRequest }); }
|
|
11829
|
+
function sessionClose(root, opts = {}) { return _sessionClose.sessionClose(root, opts, { VERSION, STATUSES, MARK, has, arg, harnessPath: __filename, readProgressRows, evidencePath, handoffPath, currentStatePath, taskLogPath, verifyRules, _autoRoadmap, _readUsageStats, readSessionCounter, writeSessionCounter, _retroAggregate, _retroOneLine, retroCmd, _loadDecisions, readRules, planPath, _loadLessons, _readFeatureGraph, _auditUserRequests, _detectDeliveredRequests, _computeRoundHistory, _computeMilestones, _computeRecentChanges, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _scanShellScriptsEncoding, _listAPISkills, _matchAPISkills, _loadShellFailures, _shellEnvDrift, _runPreWakeAudit, _saveAndAppendPreWakeReport, _runIdempotencyAudit, _detectAbnormalShutdown, _updateUserRequest, _detectOptimism, _scanCodeForPatterns, _collectSecretFindings }); } // 1.17.6 (UR-0049): 마감 정합 — done 낙관 재확인 + 시크릿 재확인
|
|
11684
11830
|
|
|
11685
11831
|
function readmeCmd(root) { syncReadme(absRoot(root)); }
|
|
11686
11832
|
function consistencyCheck(root) {
|
|
@@ -11709,13 +11855,21 @@ async function selfCheck(root) {
|
|
|
11709
11855
|
// 1.9.2: 게이트 5종 한번에 실행 (verify + audit + scan secrets + encoding check + lazy detect).
|
|
11710
11856
|
function gate(root) {
|
|
11711
11857
|
root = absRoot(root);
|
|
11712
|
-
|
|
11858
|
+
const jsonMode = has('--json'); // 외부리뷰 C2: --json 일관성 — 이전엔 텍스트 헤더+단계별 JSON 혼재로 파싱 불가. 단일 객체로 집계.
|
|
11859
|
+
const checks = [];
|
|
11713
11860
|
let bad = 0;
|
|
11861
|
+
if (!jsonMode) log('# leerness gate (5 checks)');
|
|
11714
11862
|
function step(label, fn) {
|
|
11715
|
-
log(`\n## ${label}`);
|
|
11716
11863
|
const code0 = process.exitCode || 0;
|
|
11717
|
-
|
|
11718
|
-
|
|
11864
|
+
if (!jsonMode) log(`\n## ${label}`);
|
|
11865
|
+
const orig = process.stdout.write;
|
|
11866
|
+
if (jsonMode) process.stdout.write = () => true; // 단계 하위출력 억제(JSON 오염 방지) — fn 은 동기, finally 로 복원
|
|
11867
|
+
let threw = null;
|
|
11868
|
+
try { fn(); } catch (e) { threw = e; } finally { if (jsonMode) process.stdout.write = orig; }
|
|
11869
|
+
const failed = threw != null || !!(process.exitCode && process.exitCode !== code0);
|
|
11870
|
+
if (threw && !jsonMode) fail(`${label} threw: ${threw.message}`);
|
|
11871
|
+
if (failed) bad++;
|
|
11872
|
+
checks.push({ name: label, ok: !failed, ...(threw ? { error: threw.message } : {}) });
|
|
11719
11873
|
process.exitCode = 0;
|
|
11720
11874
|
}
|
|
11721
11875
|
step('verify', () => verify(root));
|
|
@@ -11723,6 +11877,7 @@ function gate(root) {
|
|
|
11723
11877
|
step('scan secrets', () => scanSecrets(root));
|
|
11724
11878
|
step('encoding check', () => encodingCheck(root));
|
|
11725
11879
|
step('lazy detect', () => lazyDetect(root));
|
|
11880
|
+
if (jsonMode) { log(JSON.stringify({ version: VERSION, root, ok: bad === 0, total: checks.length, failed: bad, checks }, null, 2)); if (bad) process.exitCode = 1; return; }
|
|
11726
11881
|
log(`\n# gate summary: ${bad} 단계 실패`);
|
|
11727
11882
|
if (bad) process.exitCode = 1;
|
|
11728
11883
|
else ok('all gates passed');
|
|
@@ -12157,7 +12312,7 @@ function _brainstormFor(root, topic) {
|
|
|
12157
12312
|
const planText = read(planFile_brainstorm);
|
|
12158
12313
|
const milestoneBlocks = planText.split(/\n(?=### M-\d{4}\.)/);
|
|
12159
12314
|
for (const b of milestoneBlocks) {
|
|
12160
|
-
const m = b.match(/^### (M-\d{4})
|
|
12315
|
+
const m = b.match(/^### (M-\d{4})\.[ \t]*(.+?)$/m);
|
|
12161
12316
|
if (m && matches(b)) {
|
|
12162
12317
|
const idx = planText.indexOf(b);
|
|
12163
12318
|
const lineNo = idx >= 0 ? planText.slice(0, idx).split('\n').length : 0;
|
|
@@ -12452,7 +12607,7 @@ function brainstormCmd(root, topic) {
|
|
|
12452
12607
|
const planText = read(planFile_b2);
|
|
12453
12608
|
const milestoneBlocks = planText.split(/\n(?=### M-\d{4}\.)/);
|
|
12454
12609
|
for (const b of milestoneBlocks) {
|
|
12455
|
-
const m = b.match(/^### (M-\d{4})
|
|
12610
|
+
const m = b.match(/^### (M-\d{4})\.[ \t]*(.+?)$/m);
|
|
12456
12611
|
if (m && matches(b)) {
|
|
12457
12612
|
const idx = planText.indexOf(b);
|
|
12458
12613
|
const lineNo = idx >= 0 ? planText.slice(0, idx).split('\n').length : 0;
|
|
@@ -16630,7 +16785,7 @@ function _leernessIdentity() {
|
|
|
16630
16785
|
isNot: '실행기/코딩 에이전트가 아님 — 어떤 에이전트 위에도 얹는 공통 운영 계층',
|
|
16631
16786
|
tagline: '어떤 AI 코딩 에이전트에도 적용되는 범용 운영 레이어 — 기억·정책·인수인계·검증·감사',
|
|
16632
16787
|
layers: [
|
|
16633
|
-
{ key: 'memory', ko: '기억', desc: '프로젝트 상태/결정/진행을 .
|
|
16788
|
+
{ key: 'memory', ko: '기억', desc: '프로젝트 상태/결정/진행을 .harness/ 에 영속화 (task/decision/lesson/plan; 선택 state substrate 는 .leerness/)' },
|
|
16634
16789
|
{ key: 'policy', ko: '정책', desc: '8단계 권한 등급 + enforce (read-only→publish), MCP 호출 게이트' },
|
|
16635
16790
|
{ key: 'handoff', ko: '인수인계', desc: '에이전트 간 컨텍스트 표준 전달 (Claude→Codex→Goose), get_project_context 1콜 온보딩' },
|
|
16636
16791
|
{ key: 'verification', ko: '검증', desc: '근거 기반 완료 검증 (verify-claim --require-evidence) — 허위 완료 차단' },
|
|
@@ -19410,7 +19565,7 @@ async function main() {
|
|
|
19410
19565
|
const root = absRoot(arg('--path', process.cwd())); const sub = args[1] || 'show';
|
|
19411
19566
|
if (sub==='show') return planShow(root);
|
|
19412
19567
|
if (sub==='init') return planInit(root);
|
|
19413
|
-
if (sub==='add') return planAdd(root, args.slice(2).join(' ') || '새 계획');
|
|
19568
|
+
if (sub==='add') return planAdd(root, args.slice(2).join(' ').trim() || '새 계획'); // 17th 버그헌트 P2: 공백-only 제목이 || 기본값을 우회(truthy)해 plan.md 손상(파서가 다음 줄 'Status:' 흡수) → trim 후 판정
|
|
19414
19569
|
if (sub==='drop') return planDrop(root, args.slice(2).join(' ') || '드랍 항목');
|
|
19415
19570
|
if (sub==='remove') return planRemoveCmd(root, args[2]);
|
|
19416
19571
|
if (sub==='progress') return planProgress(root, { json: has('--json'), updateIntent: args.slice(2).some(a => /^M-\d/i.test(a)) || has('--status') || arg('--progress', null) != null }); // 1.9.447 (UR-0145): --json + 변경의도 인자 경고
|
|
@@ -19505,6 +19660,14 @@ async function main() {
|
|
|
19505
19660
|
}
|
|
19506
19661
|
// 1.9.306 (UR-0045): 명시적 help 요청은 exit 0, 그 외 미인식 명령은 안내 + exit 1 (실패를 성공으로 오판 방지).
|
|
19507
19662
|
if (cmd === 'help' || cmd === 'commands' || cmd === '--help' || cmd === '-h') { help(); return; }
|
|
19663
|
+
// 1.16.2 (외부클린룸 UR-0042): 유효 명령그룹을 하위명령 없이 부르면 'unknown command'(혼란) 대신 사용법 힌트 — decision/lesson 과 일관.
|
|
19664
|
+
const _GROUP_USAGE = {
|
|
19665
|
+
rule: 'rule add "<텍스트>" --trigger <트리거> | rule list | rule pause/resume/remove <ID> | rule verify',
|
|
19666
|
+
skill: 'skill list | skill add <id> | skill use <id> | skill search "<키>" | skill match "<텍스트>"',
|
|
19667
|
+
feature: 'feature add "<이름>" | feature list | feature show <ID> | feature link <A> <B> | feature impact <ID>',
|
|
19668
|
+
memory: 'memory search "<키>" [--json] | memory status | memory archive | memory restore',
|
|
19669
|
+
};
|
|
19670
|
+
if (_GROUP_USAGE[cmd] && !args[1]) { failJson(has('--json'), 'subcommand_required', `${cmd} 하위명령 필요 — 사용법: leerness ${_GROUP_USAGE[cmd]}`); return; }
|
|
19508
19671
|
// 1.9.437 (11th 외부평가 Codex P2, UR-0138): --json 모드 unknown command 도 순수 JSON.
|
|
19509
19672
|
failJson(has('--json'), 'unknown_command', `알 수 없는 명령: ${cmd} (leerness --help 로 전체 명령 확인)`);
|
|
19510
19673
|
return;
|
package/lib/pure-utils.js
CHANGED
|
@@ -322,7 +322,7 @@ function _roadmapParseMilestones(text) {
|
|
|
322
322
|
const s = String(text || '');
|
|
323
323
|
const out = [];
|
|
324
324
|
// 1.9.352 (UR-0068 외부리뷰): 다음 milestone 직전까지 block 한정 — 이전 구현은 slice(m.index) 로 다음 milestone 의 Status/Progress 를 누출했음
|
|
325
|
-
const matches = [...s.matchAll(/^### (M-\d{4})
|
|
325
|
+
const matches = [...s.matchAll(/^### (M-\d{4})\.[ \t]*(.+?)$/gm)]; // 17th 버그헌트 P2: \s* 가 개행 흡수해 빈 제목 milestone 이 다음 줄(Status:)을 제목으로 먹던 것 차단
|
|
326
326
|
for (let i = 0; i < matches.length; i++) {
|
|
327
327
|
const m = matches[i];
|
|
328
328
|
const end = i + 1 < matches.length ? matches[i + 1].index : s.length;
|
package/lib/session-close.js
CHANGED
|
@@ -11,7 +11,7 @@ const { log, ok, warn, fail, failJson, today, now, absRoot, exists, read, readBu
|
|
|
11
11
|
const { _sanitizeFences, _parseArchiveBlocks } = require('./pure-utils');
|
|
12
12
|
|
|
13
13
|
function sessionClose(root, opts = {}, deps = {}) {
|
|
14
|
-
const { VERSION, STATUSES, MARK, has, arg, harnessPath, readProgressRows, evidencePath, handoffPath, currentStatePath, taskLogPath, verifyRules, _autoRoadmap, _readUsageStats, readSessionCounter, writeSessionCounter, _retroAggregate, _retroOneLine, retroCmd, _loadDecisions, readRules, planPath, _loadLessons, _readFeatureGraph, _auditUserRequests, _detectDeliveredRequests, _computeRoundHistory, _computeMilestones, _computeRecentChanges, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _scanShellScriptsEncoding, _listAPISkills, _matchAPISkills, _loadShellFailures, _shellEnvDrift, _runPreWakeAudit, _saveAndAppendPreWakeReport, _runIdempotencyAudit, _detectAbnormalShutdown, _updateUserRequest } = deps;
|
|
14
|
+
const { VERSION, STATUSES, MARK, has, arg, harnessPath, readProgressRows, evidencePath, handoffPath, currentStatePath, taskLogPath, verifyRules, _autoRoadmap, _readUsageStats, readSessionCounter, writeSessionCounter, _retroAggregate, _retroOneLine, retroCmd, _loadDecisions, readRules, planPath, _loadLessons, _readFeatureGraph, _auditUserRequests, _detectDeliveredRequests, _computeRoundHistory, _computeMilestones, _computeRecentChanges, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _scanShellScriptsEncoding, _listAPISkills, _matchAPISkills, _loadShellFailures, _shellEnvDrift, _runPreWakeAudit, _saveAndAppendPreWakeReport, _runIdempotencyAudit, _detectAbnormalShutdown, _updateUserRequest, _detectOptimism, _scanCodeForPatterns, _collectSecretFindings } = deps;
|
|
15
15
|
root = absRoot(root);
|
|
16
16
|
// 1.10.4 (13th 버그헌트 P2, UR-0167): 경로 없음/디렉토리 아님 → 구조화 에러 + exit 1. mkdir <path>/.harness ENOTDIR 크래시 & 실패를 성공(exit 0)으로 오판하던 문제 차단.
|
|
17
17
|
if (!exists(root) || !fs.statSync(root).isDirectory()) { failJson(!!opts.json || has('--json'), 'path_not_found', `경로 없음 또는 디렉토리 아님: ${root}`); return; }
|
|
@@ -34,6 +34,22 @@ function sessionClose(root, opts = {}, deps = {}) {
|
|
|
34
34
|
const _doneNoEvidence = (buckets['done'] || []).filter(r => !r.evidence || /^(\s*|user-request|-)$/.test(r.evidence) || /^plan:M-\d{4}\s*$/.test(r.evidence));
|
|
35
35
|
jsonResult.completionHonesty = { doneTotal: (buckets['done'] || []).length, doneWithoutEvidence: _doneNoEvidence.length, ids: _doneNoEvidence.slice(0, 5).map(r => r.id) };
|
|
36
36
|
if (_doneNoEvidence.length) log(` ⚠ 완료 정직성: done ${_doneNoEvidence.length}건 evidence 없음/placeholder (${_doneNoEvidence.slice(0, 3).map(r => r.id).join(', ')}) — verify-claim 권장 (advisory)`);
|
|
37
|
+
// 1.17.6 (UR-0049 마감 정합): done 의 미해소 낙관 의심 재확인 — verify-claim 을 건너뛴 거짓 주장(evidence 에 API/DB 주장 있는데 코드 흔적 없음)이
|
|
38
|
+
// 평범한 'done' 으로 마감을 무사 통과하던 것(5축 실증 P2: 거짓 DB 주장이 done 으로 마감, gate 실패 중 'clean' 선언) — 마감이 마지막 관문 역할을 하도록 재확인. advisory.
|
|
39
|
+
let _doneOptimism = [];
|
|
40
|
+
try {
|
|
41
|
+
if (_detectOptimism && _scanCodeForPatterns && (buckets['done'] || []).length) {
|
|
42
|
+
const _codeText = _scanCodeForPatterns(root);
|
|
43
|
+
_doneOptimism = (buckets['done'] || []).map(r => ({ id: r.id, suspects: _detectOptimism(r.evidence || '', _codeText) || [] })).filter(x => x.suspects.length);
|
|
44
|
+
}
|
|
45
|
+
} catch {}
|
|
46
|
+
jsonResult.completionHonesty.optimismUnresolved = _doneOptimism.map(x => ({ id: x.id, kinds: x.suspects.map(s => s.kind) }));
|
|
47
|
+
if (_doneOptimism.length) log(` ⚠ 완료 정직성: done ${_doneOptimism.length}건 낙관 의심 미해소 (${_doneOptimism.slice(0, 3).map(x => x.id).join(', ')}) — evidence 주장 vs 코드 흔적 불일치, 마감 전 verify-claim 재확인 권장 (advisory)`);
|
|
48
|
+
// 1.17.6 (UR-0049): 마감 보안 재확인 — 커밋 대상 시크릿이 살아있으면 'clean' 으로 마감하지 않도록 표면화. advisory(차단 X).
|
|
49
|
+
let _closeSecrets = 0;
|
|
50
|
+
try { if (_collectSecretFindings) _closeSecrets = ((_collectSecretFindings(root) || {}).committed || []).length; } catch {}
|
|
51
|
+
jsonResult.closeSecurity = { committedSecrets: _closeSecrets };
|
|
52
|
+
if (_closeSecrets) log(` 🚨 마감 보안: 커밋 대상 시크릿 ${_closeSecrets}건 미해소 — clean 아님, leerness scan secrets 확인 후 마감 권장`);
|
|
37
53
|
|
|
38
54
|
function rowsToList(arr) {
|
|
39
55
|
if (!arr || !arr.length) return '- 없음';
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -451,9 +451,11 @@ total++;
|
|
|
451
451
|
fs.writeFileSync(path.join(tmpV, 'src/myMod.js'), 'module.exports = {};\n');
|
|
452
452
|
fs.writeFileSync(path.join(tmpV, 'tests/test.js'), 'check(1); check(2); check(3); check(4); check(5);\n');
|
|
453
453
|
// T-row를 evidence와 함께 추가
|
|
454
|
+
// 1.17.4 (UR-0047): evidence 에 명시적 개수 주장(테스트 5개) 포함 — 카운트 검증이 실제로 수행되는 경로를 테스트.
|
|
455
|
+
// 이전 evidence 는 "(5/5 통과)"(pass 비율)만 있어 개수 주장이 없었는데도 옛 코드가 "✓ pass (실측 ≥ 주장)" 으로 표기(측정실패=통과 모순의 일부) — 정직화로 "⊘ (주장 없음)" 이 되므로 의도(카운트 검증)에 맞게 주장을 명시.
|
|
454
456
|
fs.appendFileSync(path.join(tmpV, '.harness/progress-tracker.md'),
|
|
455
|
-
'| T-0099 | done | 신모듈 | src/myMod.js + tests/test.js (5/5 통과) | next | 2026-05-14 |\n');
|
|
456
|
-
// 정상: 파일 존재 + 테스트 5개
|
|
457
|
+
'| T-0099 | done | 신모듈 | src/myMod.js + tests/test.js 테스트 5개 (5/5 통과) | next | 2026-05-14 |\n');
|
|
458
|
+
// 정상: 파일 존재 + 테스트 5개 (주장 5 = 실측 5)
|
|
457
459
|
const r = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0099', '--path', tmpV], { encoding: 'utf8', timeout: 15000 });
|
|
458
460
|
const okPass = r.status === 0 && /✓ src\/myMod\.js/.test(r.stdout) && /✓ tests\/test\.js/.test(r.stdout) && /pass \(실측 ≥ 주장\)/.test(r.stdout);
|
|
459
461
|
// 파일 없는 케이스 → exit ≠ 0
|