leerness 1.36.55 → 1.36.57
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 +20 -0
- package/README.md +4 -4
- package/bin/leerness.js +163 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.57 — 2026-07-21 — integrity check --repair: managed 문서 무결성 복구 (외부감사 F-04) — R-0001 검수 2회전
|
|
4
|
+
|
|
5
|
+
drift(신선도)와 별개로 "파일 자체가 잘리거나 사라진" 손상을 감지/복구. R-0001 codex 검수가 High 2 포함 5건을 지적 — 전건 재현·보정 후 재검(minimal 오판·repair 실패 exit 0 는 검수 없이는 출하됐을 결함).
|
|
6
|
+
|
|
7
|
+
- **`leerness integrity check [--repair] [--force] [--json]`**: 정책-문서 12종(사용자 데이터 표면 제외) — 부재/H1 전무/템플릿 40% 미만 절단 감지. --repair 는 원본 archive 대피(pid+seq 접미, 검수 #4) 후 현재 버전 템플릿 재생성.
|
|
8
|
+
- (검수 #1 High) **minimal 설치 존중**: manifest.minimal 반영 — minimal 이 뺀 6종을 '부재 손상' 오판/재생성하지 않음(6종만 검사).
|
|
9
|
+
- (검수 #2 High) **repair 실패 = exit 1**: 종전엔 복구 실패(EEXIST 등)에도 exit 0.
|
|
10
|
+
- (검수 #3) **오탐 완화**: H1 판정을 "H1 전무"로(제목 번역/개명 무탐), 길이-단독 신호(truncated)는 advisory — --force 없인 복구/exit 비관여(합리적 축약 보호).
|
|
11
|
+
- (검수 #5) unknown 하위명령 JSON 오류 + templateBytes 실제 바이트.
|
|
12
|
+
- 실측: 감사 시나리오(context-routing 절단) 감지→복구, 사용자 추가 편집 무탐, en/ko 라운드트립 클린. selftest 330(+1).
|
|
13
|
+
|
|
14
|
+
## 1.36.56 — 2026-07-21 — 시크릿 스캐너 F-06 (외부감사 P1) + 매 라운드 codex 검수 룰 (R-0001, 사용자 지시)
|
|
15
|
+
|
|
16
|
+
사용자 지시로 영구 룰 R-0001(every-round: 출하 전 codex 교차 검수) 등록 — 이 릴리스가 첫 적용: 1차 구현 → codex 검수가 4건 지적 → 전건 재현 확인 후 보정 → 재검.
|
|
17
|
+
|
|
18
|
+
- **무명 확장자 소형 텍스트 스캔**: `config.custom`/확장자 없는 credential 파일 미탐 해소 — ≤64KB + NUL 스니핑 + 이진 확장자 deny(검수 #4: .bin/.dat 등 추가). (검수 #2) 확장자 없는 파일('')이 기존 무제한 경로로 새던 것을 가드 경로로 통일 — 대형/이진 무명 파일도 이제 안전장치 적용.
|
|
19
|
+
- **중복 보고 dedupe**: (검수 #1) 시작-오프셋 키는 실제 중복(컨텍스트 패턴=키 시작 vs 직접 패턴=값 시작)을 못 접었다 — **값 스팬(시작+길이) 키**로 보정. 실측: `api_key = "npm_…"` 2건→1건, 서로 다른 토큰 2건 유지.
|
|
20
|
+
- **성능**: (검수 #3) 파일당 stat 2회+.gitignore 재읽기(5천 파일 실측 5천회) — stat 1회 + .gitignore 수집당 1회 캐시.
|
|
21
|
+
- 검증: selftest 329(+1: 동일토큰 1건/통제군/무확장 64KB·NUL 가드), 게이트, R-0001 검수 사이클 1회전 완료.
|
|
22
|
+
|
|
3
23
|
## 1.36.55 — 2026-07-21 — E2E 아티팩트 이식성: 게시본에서 release gate 재현 (외부 GPT 감사 P0-2)
|
|
4
24
|
|
|
5
25
|
감사 지적: 게시 npm 아티팩트에서 e2e 6건이 저장소-전용 픽스처로 실패 — "release gate 가 아티팩트에서 재현 안 되는 것은 릴리스 엔지니어링 결함". 6건 전부 자기완결화.
|
package/README.md
CHANGED
|
@@ -122,7 +122,7 @@ MIT
|
|
|
122
122
|
<!-- leerness:project-readme:start -->
|
|
123
123
|
## Leerness Project Harness
|
|
124
124
|
|
|
125
|
-
이 프로젝트는 Leerness v1.36.
|
|
125
|
+
이 프로젝트는 Leerness v1.36.57 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
126
126
|
|
|
127
127
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
128
128
|
|
|
@@ -176,7 +176,7 @@ leerness memory restore decision <date|title>
|
|
|
176
176
|
|
|
177
177
|
### MCP server (외부 AI 통합)
|
|
178
178
|
|
|
179
|
-
Leerness v1.36.
|
|
179
|
+
Leerness v1.36.57는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
|
|
180
180
|
|
|
181
181
|
```jsonc
|
|
182
182
|
// 카테고리별
|
|
@@ -197,7 +197,7 @@ Leerness v1.36.55는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
197
197
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
198
198
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) 회귀 테스트 갱신 → 4) 전체 e2e 스위트 통과 → 5) npm publish + git tag → 6) main push → 7) session close → 8) 다음 라운드 예약.
|
|
199
199
|
|
|
200
|
-
현재 누적: **v1.9.x → 1.36.
|
|
200
|
+
현재 누적: **v1.9.x → 1.36.57 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
|
|
201
201
|
|
|
202
202
|
### 성능 가이드
|
|
203
203
|
|
|
@@ -235,6 +235,6 @@ leerness release pack --close --auto-main-push
|
|
|
235
235
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
236
236
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
237
237
|
|
|
238
|
-
Last synced by Leerness v1.36.
|
|
238
|
+
Last synced by Leerness v1.36.57: 2026-07-21
|
|
239
239
|
<!-- leerness:project-readme:end -->
|
|
240
240
|
|
package/bin/leerness.js
CHANGED
|
@@ -34,7 +34,7 @@ const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE
|
|
|
34
34
|
const { tokenizeForRank: _tokenizeForRank, expandQuery: _expandQuery, scoreHits: _scoreHits, suggestTerms: _suggestTerms } = require('../lib/search-core'); // 1.36.23: memory search 랭킹 코어(순수·0-deps)
|
|
35
35
|
const { findCorruptedStateJson: _findCorruptedStateJson } = require('../lib/state-integrity'); // 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태 무결성 (audit/health/check 공유)
|
|
36
36
|
|
|
37
|
-
const VERSION = '1.36.
|
|
37
|
+
const VERSION = '1.36.57';
|
|
38
38
|
|
|
39
39
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
40
40
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -5126,6 +5126,61 @@ function _selfTestCases() {
|
|
|
5126
5126
|
&& io2.includes('!_jsonErrEmitted && t && (t[0]') && /process\.on\('exit', \(code\) => \{ if \(code !== 0/.test(io2) && io2.includes('.corrupt-${Date.now()}-${process.pid}-')
|
|
5127
5127
|
&& s.includes('declaredTestCount != null && runResult && !runResult.skipped && runResult.allPassed');
|
|
5128
5128
|
return clar && shape && noDbUrl && reqOk && guards;
|
|
5129
|
+
} },
|
|
5130
|
+
{ name: '시크릿 스캐너 F-06 (1.36.56, 외부감사): 무명 확장자 소형 텍스트 스캔(이진 NUL 제외) + 같은 토큰 중복 보고 dedupe — 행위검사', run: () => {
|
|
5131
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_sc56_'));
|
|
5132
|
+
try {
|
|
5133
|
+
fs.writeFileSync(path.join(tmp, 'config.custom'), 'SERVICE_TOKEN = "npm_abcdefghijklmnopqrstuvwxyz0123456789"\n');
|
|
5134
|
+
fs.writeFileSync(path.join(tmp, 'CREDENTIALS'), 'password=hunter2realsecret\n');
|
|
5135
|
+
fs.writeFileSync(path.join(tmp, 'blob.custom'), Buffer.from([0, 1, 2, 3, 0, 5])); // 이진 → 제외
|
|
5136
|
+
fs.writeFileSync(path.join(tmp, 'sample.custom'), 'API_KEY=your-key-here\n'); // placeholder → 제외
|
|
5137
|
+
const r = _collectSecretFindings(tmp);
|
|
5138
|
+
const files = r.committed.map(f => f.file);
|
|
5139
|
+
const unknownOk = files.includes('config.custom') && files.includes('CREDENTIALS') && !files.includes('blob.custom') && !files.includes('sample.custom');
|
|
5140
|
+
// dedupe: 같은 토큰이 직접 패턴(npm 접두)과 컨텍스트 패턴(할당) 둘 다에 걸려도 1건 (검수 #1 실측 케이스)
|
|
5141
|
+
fs.writeFileSync(path.join(tmp, 'same.js'), 'api_key = "npm_a1B2a1B2a1B2a1B2a1B2a1B2a1B2a1B2a1B2"\n');
|
|
5142
|
+
const rSame = _collectSecretFindings(path.join(tmp, 'same.js'));
|
|
5143
|
+
const sameTokenOnce = rSame.committed.length === 1;
|
|
5144
|
+
// 서로 다른 토큰 2개는 유지 (통제군)
|
|
5145
|
+
fs.writeFileSync(path.join(tmp, 'dup.js'), 'const s = { password: "realpw123456", api_key: "realkey7890123" };\n');
|
|
5146
|
+
const r2 = _collectSecretFindings(path.join(tmp, 'dup.js'));
|
|
5147
|
+
const distinctKept = r2.committed.length === 2 && new Set(r2.committed.map(f => f.snippet)).size === 2;
|
|
5148
|
+
// 검수 #2: 확장자 없는 파일도 64KB/NUL 가드 적용 (대형/이진 무명 파일 제외)
|
|
5149
|
+
fs.writeFileSync(path.join(tmp, 'BIGNOEXT'), 'x'.repeat(64 * 1024 + 1) + ' password=hunterreal99\n');
|
|
5150
|
+
fs.writeFileSync(path.join(tmp, 'NULNOEXT'), Buffer.concat([Buffer.from([65, 66, 0, 67]), Buffer.from('password=hunterreal99\n')]));
|
|
5151
|
+
const r3 = _collectSecretFindings(tmp);
|
|
5152
|
+
const extlessGuard = !r3.committed.some(f => f.file === 'BIGNOEXT' || f.file === 'NULNOEXT');
|
|
5153
|
+
const srcOk = read(__filename).includes('_seenAt.has(_valKey)') && read(__filename).includes('_giMatch(fileRel)');
|
|
5154
|
+
return unknownOk && sameTokenOnce && distinctKept && extlessGuard && srcOk;
|
|
5155
|
+
} finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
5156
|
+
} },
|
|
5157
|
+
{ name: 'integrity check/repair (1.36.57, 외부감사 F-04): 절단/부재 감지 → archive 대피 후 템플릿 재생성 · 사용자 추가편집 무탐 — 행위검사', run: () => {
|
|
5158
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_int_'));
|
|
5159
|
+
const _w = process.stdout.write; const saveArgv = process.argv; const saveExit = process.exitCode;
|
|
5160
|
+
try {
|
|
5161
|
+
process.stdout.write = () => true;
|
|
5162
|
+
fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true });
|
|
5163
|
+
const tf = coreFiles(tmp, 'ko', [], {});
|
|
5164
|
+
for (const k of _INTEGRITY_DOCS) fs.mkdirSync(path.dirname(path.join(tmp, k)), { recursive: true }), fs.writeFileSync(path.join(tmp, k), tf[k]);
|
|
5165
|
+
fs.writeFileSync(path.join(tmp, '.harness', 'manifest.json'), '{"language":"ko"}');
|
|
5166
|
+
// 손상 주입: 절단 + 삭제 / 사용자 추가 편집(무탐 대상)
|
|
5167
|
+
fs.writeFileSync(path.join(tmp, '.harness', 'context-routing.md'), 'x');
|
|
5168
|
+
fs.rmSync(path.join(tmp, '.harness', 'guardrails.md'));
|
|
5169
|
+
fs.appendFileSync(path.join(tmp, '.harness', 'testing-strategy.md'), '\n## 커스텀 절차\n- 내 규칙\n');
|
|
5170
|
+
process.argv = ['node', 'x', 'integrity', 'check'];
|
|
5171
|
+
process.exitCode = 0; integrityCmd(tmp);
|
|
5172
|
+
const detected = process.exitCode === 1;
|
|
5173
|
+
process.argv = ['node', 'x', 'integrity', 'check', '--repair'];
|
|
5174
|
+
process.exitCode = 0; integrityCmd(tmp);
|
|
5175
|
+
const routing = fs.readFileSync(path.join(tmp, '.harness', 'context-routing.md'), 'utf8');
|
|
5176
|
+
const repaired = routing.includes('# Context Routing') && fs.existsSync(path.join(tmp, '.harness', 'guardrails.md'));
|
|
5177
|
+
const backed = fs.readdirSync(path.join(tmp, '.harness', 'archive')).some(n => n.startsWith('integrity-context-routing'));
|
|
5178
|
+
const customKept = fs.readFileSync(path.join(tmp, '.harness', 'testing-strategy.md'), 'utf8').includes('커스텀 절차');
|
|
5179
|
+
process.argv = ['node', 'x', 'integrity', 'check'];
|
|
5180
|
+
process.exitCode = 0; integrityCmd(tmp);
|
|
5181
|
+
const cleanAfter = process.exitCode !== 1;
|
|
5182
|
+
return detected && repaired && backed && customKept && cleanAfter;
|
|
5183
|
+
} finally { process.stdout.write = _w; process.argv = saveArgv; process.exitCode = saveExit; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
5129
5184
|
} }
|
|
5130
5185
|
];
|
|
5131
5186
|
}
|
|
@@ -5499,6 +5554,79 @@ function anchorsCmd(root, sub) {
|
|
|
5499
5554
|
fail(`알 수 없는 하위명령: ${sub} (가능: status, draft [--apply])`);
|
|
5500
5555
|
}
|
|
5501
5556
|
|
|
5557
|
+
// 1.36.57 (외부 GPT 감사 F-04): managed 정책-문서 무결성 점검/복구 — drift(신선도)와 별개로 "파일 자체가
|
|
5558
|
+
// 잘리거나 사라진" 손상을 감지한다. 대상은 사용자가 편집하지 않는 순수 템플릿 문서군만(사용자 데이터 표면 제외 —
|
|
5559
|
+
// plan/progress/brief/decisions 등은 절대 건드리지 않음). 판정은 보수적: 부재 / H1 상실 / 템플릿 대비 40% 미만 크기.
|
|
5560
|
+
// --repair 는 손상 원본을 .harness/archive/ 로 대피 후 현재 버전 템플릿으로 재생성.
|
|
5561
|
+
const _INTEGRITY_DOCS = [
|
|
5562
|
+
'.harness/session-workflow.md', '.harness/context-routing.md', '.harness/writeback-policy.md',
|
|
5563
|
+
'.harness/task-type-map.md', '.harness/protected-files.md', '.harness/guardrails.md',
|
|
5564
|
+
'.harness/consistency-policy.md', '.harness/testing-strategy.md', '.harness/review-checklist.md',
|
|
5565
|
+
'.harness/release-checklist.md', '.harness/session-close-policy.md', '.harness/plan-progress-boundary.md',
|
|
5566
|
+
];
|
|
5567
|
+
function integrityCmd(root) {
|
|
5568
|
+
root = absRoot(root);
|
|
5569
|
+
const json = has('--json');
|
|
5570
|
+
const repair = has('--repair');
|
|
5571
|
+
if (!exists(path.join(root, '.harness'))) { failJson(json, 'harness_missing', `leerness 미설치: ${root} — 먼저 leerness init`); return; }
|
|
5572
|
+
let lang = 'ko', minimal = false;
|
|
5573
|
+
try { const mf = JSON.parse(read(path.join(root, '.harness', 'manifest.json'))); lang = mf.language || 'ko'; minimal = !!mf.minimal; } catch {}
|
|
5574
|
+
// (검수 #1) minimal 설치 존중 — minimal 이 의도적으로 뺀 문서를 '부재 손상'으로 오판/재생성하지 않는다
|
|
5575
|
+
const templates = coreFiles(root, lang, [], { minimal });
|
|
5576
|
+
const docs = _INTEGRITY_DOCS.filter(k => typeof templates[k] === 'string');
|
|
5577
|
+
const findings = []; const repaired = []; let repairFailed = false;
|
|
5578
|
+
const force = has('--force');
|
|
5579
|
+
for (const key of docs) {
|
|
5580
|
+
const tmpl = templates[key];
|
|
5581
|
+
const fp = path.join(root, key);
|
|
5582
|
+
let kind = null;
|
|
5583
|
+
if (!exists(fp)) kind = 'missing';
|
|
5584
|
+
else {
|
|
5585
|
+
let cur = ''; try { cur = read(fp); } catch { kind = 'unreadable'; }
|
|
5586
|
+
if (!kind) {
|
|
5587
|
+
// (검수 #3) H1 상실 = "H1 이 하나도 없음"(정확 일치 아님 — 사용자가 제목을 번역/개명해도 무탐)
|
|
5588
|
+
if (!cur.split('\n').some(l => /^# \S/.test(l.trim()))) kind = 'h1_lost';
|
|
5589
|
+
else if (cur.length < tmpl.length * 0.4) kind = 'truncated';
|
|
5590
|
+
}
|
|
5591
|
+
}
|
|
5592
|
+
if (!kind) continue;
|
|
5593
|
+
// (검수 #3) 길이-단독 신호(truncated)는 advisory — --force 없인 복구도 exit 1 도 아님 (합리적 축약 오판 방지)
|
|
5594
|
+
const advisory = kind === 'truncated' && !force;
|
|
5595
|
+
const f = { file: key, kind, advisory, templateBytes: Buffer.byteLength(tmpl, 'utf8') };
|
|
5596
|
+
if (repair && !advisory) {
|
|
5597
|
+
try {
|
|
5598
|
+
if (exists(fp)) {
|
|
5599
|
+
const bakDir = path.join(root, '.harness', 'archive');
|
|
5600
|
+
mkdirp(bakDir);
|
|
5601
|
+
// (검수 #4) 같은 ms 백업 충돌 방지 — pid+시퀀스 접미
|
|
5602
|
+
fs.copyFileSync(fp, path.join(bakDir, `integrity-${path.basename(key)}-${Date.now()}-${process.pid}-${++_integSeq}.bak`));
|
|
5603
|
+
f.backedUp = true;
|
|
5604
|
+
}
|
|
5605
|
+
writeUtf8(fp, tmpl);
|
|
5606
|
+
f.repaired = true; repaired.push(key);
|
|
5607
|
+
} catch (e) { f.repaired = false; f.error = String(e.message || e).slice(0, 120); repairFailed = true; }
|
|
5608
|
+
}
|
|
5609
|
+
findings.push(f);
|
|
5610
|
+
}
|
|
5611
|
+
// (검수 #2) 실패한 repair 는 성공이 아니다 — hard 손상 잔존 또는 복구 실패 시 exit 1 (advisory 는 exit 무영향)
|
|
5612
|
+
const hardLeft = findings.filter(f => !f.advisory && !f.repaired);
|
|
5613
|
+
if (json) {
|
|
5614
|
+
log(JSON.stringify({ ok: !repairFailed && hardLeft.length === 0, checked: docs.length, minimal, findings, repaired }, null, 2));
|
|
5615
|
+
if (repairFailed || hardLeft.length) process.exitCode = 1;
|
|
5616
|
+
return;
|
|
5617
|
+
}
|
|
5618
|
+
log(`# leerness integrity — managed 정책-문서 무결성 (${docs.length}종 검사${minimal ? ' · minimal' : ''})`);
|
|
5619
|
+
if (!findings.length) { ok(' 손상 없음 — 전 문서 구조 정상'); return; }
|
|
5620
|
+
for (const f of findings) {
|
|
5621
|
+
const icon = f.repaired ? '🔧 복구' : (f.advisory ? '⚠ 의심' : '✗ 손상');
|
|
5622
|
+
log(` ${icon} ${f.file} (${f.kind})${f.repaired && f.backedUp ? ' — 원본은 .harness/archive/ 대피' : ''}${f.advisory ? ' — 길이-단독 신호(advisory): 복구하려면 --repair --force' : ''}${f.error ? ` — 복구 실패: ${f.error}` : ''}`);
|
|
5623
|
+
}
|
|
5624
|
+
if (hardLeft.length && !repair) warn(` 복구: leerness integrity check --repair (손상 원본은 archive 로 대피 후 템플릿 재생성)`);
|
|
5625
|
+
if (repairFailed || hardLeft.length) process.exitCode = 1;
|
|
5626
|
+
return;
|
|
5627
|
+
}
|
|
5628
|
+
let _integSeq = 0;
|
|
5629
|
+
|
|
5502
5630
|
// 1.36.28 (codex 미검토표면 헌트 #3 — 데이터 유실 클래스): 손상된 JSON 스토어를 "빈 값"으로 오인해 덮어쓰는 fail-open 방지.
|
|
5503
5631
|
// 파일이 "없음"과 "있으나 손상"을 구분 — 후자는 파싱 실패를 던져 변경(저장)을 거부하고 원본을 보존한다.
|
|
5504
5632
|
// 설계: LOAD 가 아니라 SAVE/변경 진입점에서만 던진다(handoff 등 비변경 읽기 경로는 resilient 유지). state 는
|
|
@@ -6266,6 +6394,7 @@ function commandsCmd(root) {
|
|
|
6266
6394
|
{ cmd: 'review-request "<request>"', desc: '사용자 요청 사전 검토 (1.9.176)' },
|
|
6267
6395
|
{ cmd: 'clarify "<사용자 요청>" [--json]', desc: '요청 모호성 신호 감지 → 사용자에게 물을 질문 생성 (추측 구현 방지) — 1.36.51 UR-0061' },
|
|
6268
6396
|
{ cmd: 'tech [--json]', desc: '기술 프로필 — 개발 언어·연결 서비스 자동 감지 + 마이그레이션/언어전환 이력, 그래프 🛠 탭 표시 — 1.36.53 UR-0062' },
|
|
6397
|
+
{ cmd: 'integrity check [--repair] [--json]', desc: 'managed 정책-문서 12종 무결성(부재/H1 상실/절단) 점검 — --repair: archive 대피 후 템플릿 재생성 — 1.36.57 감사 F-04' },
|
|
6269
6398
|
{ cmd: 'preview add|list|show|approve|revise', desc: '신규 기능 미리보기 승인 워크플로 — approve 전 코드 작성 금지 계약 — 1.36.51 UR-0061' },
|
|
6270
6399
|
{ cmd: 'review <file> --persona <ids>', desc: '페르소나 리뷰 (1.9.29)' },
|
|
6271
6400
|
{ cmd: 'brainstorm "<topic>" [--include-code]', desc: '워크스페이스 회수 + 코드 grep' }
|
|
@@ -9763,6 +9892,9 @@ function _isLikelyGitignored(root, fileRel) {
|
|
|
9763
9892
|
function _collectSecretFindings(root) {
|
|
9764
9893
|
root = absRoot(root);
|
|
9765
9894
|
const findings = [];
|
|
9895
|
+
// 1.36.56 (검수 #3): .gitignore 는 수집당 1회만 로드 — 종전엔 적격 파일마다 재읽기(5천 파일 실측 5천회)
|
|
9896
|
+
let _giText = null; try { _giText = read(path.join(root, '.gitignore')); } catch {}
|
|
9897
|
+
const _giMatch = (fileRel) => _giText != null && _gitignoreMatch(_giText, fileRel);
|
|
9766
9898
|
// 1.9.354 (UR-0072 외부리뷰): root 가 파일이면 그 파일만 스캔. 디렉토리면 walk.
|
|
9767
9899
|
let _iter;
|
|
9768
9900
|
try { _iter = fs.statSync(root).isFile() ? [root] : walk(root); } catch { _iter = walk(root); }
|
|
@@ -9772,16 +9904,32 @@ function _collectSecretFindings(root) {
|
|
|
9772
9904
|
const isEnvFamily = /^\.env(\.|$)/.test(path.basename(file));
|
|
9773
9905
|
// 1.26.1 (13번째 외부리뷰 P2): 개인키/인증서 파일(.pem/.key/.crt/.p8 …)은 확장자 allow-list 에 없어 스캔 누락 → 커밋된 개인키 미탐 + handoff 'OK' 거짓보증. basename 으로 강제 포함('Generic private key' 정규식이 실제로 돌도록).
|
|
9774
9906
|
const isKeyFile = /\.(?:pem|key|crt|cer|der|p8|p12|pfx|pkcs8|ppk|asc|gpg|keystore|jks)$/i.test(path.basename(file));
|
|
9775
|
-
|
|
9907
|
+
// 1.36.56 (외부 GPT 감사 F-06): 알 수 없는 확장자도 "작고(≤64KB) 텍스트인" 파일은 스캔 —
|
|
9908
|
+
// config.custom / 확장자 없는 credential 파일 미탐을 막는다. 이진 파일은 확장자 deny + NUL 스니핑으로 제외.
|
|
9909
|
+
// (검수 #2) 확장자 없는 파일('')은 SCAN_TEXT_EXT 의 '' 무제한 경로가 아니라 이 가드 경로로 — 64KB/NUL 안전장치가 실제 적용되게.
|
|
9910
|
+
// (검수 #3) 확장자 deny 를 stat 보다 먼저, stat 은 1회만.
|
|
9911
|
+
const _binExt = /\.(?:png|jpe?g|gif|webp|ico|bmp|pdf|zip|gz|tgz|7z|rar|exe|dll|so|dylib|woff2?|ttf|eot|otf|mp[34]|wav|mov|avi|wasm|jar|class|pyc|db|sqlite3?|bin|dat|pak|iso|img)$/i.test(ext);
|
|
9912
|
+
const _known = (ext !== '' && SCAN_TEXT_EXT.has(ext)) || isEnvFamily || isKeyFile;
|
|
9913
|
+
if (!_known && _binExt) continue;
|
|
9914
|
+
let st; try { st = fs.statSync(file); } catch { continue; } // 1.12.5: stat-before-read (1회)
|
|
9915
|
+
if (st.size > 1024 * 1024) continue;
|
|
9776
9916
|
let text;
|
|
9777
|
-
|
|
9778
|
-
|
|
9779
|
-
|
|
9917
|
+
if (!_known) {
|
|
9918
|
+
if (st.size === 0 || st.size > 64 * 1024) continue; // 소형 파일만 (감사 권고)
|
|
9919
|
+
let buf; try { buf = fs.readFileSync(file); } catch { continue; }
|
|
9920
|
+
if (buf.subarray(0, 512).includes(0)) continue; // NUL = 이진
|
|
9921
|
+
text = buf.toString('utf8'); if (text.charCodeAt(0) === 0xFEFF) text = text.slice(1);
|
|
9922
|
+
} else {
|
|
9923
|
+
try { text = read(file); } catch { continue; }
|
|
9924
|
+
}
|
|
9780
9925
|
if (text.length > 1024 * 1024) continue;
|
|
9781
9926
|
const fileRel = (file === root) ? path.basename(file) : rel(root, file);
|
|
9782
9927
|
// 1.9.350 (UR-0060): leerness 자기 harness.js + secret-policy 템플릿만 제외.
|
|
9783
9928
|
if (path.resolve(file) === path.resolve(__filename) || /(^|[\\/])\.(?:harness|leerness)[\\/]secret-policy\.md$/.test(fileRel)) continue;
|
|
9784
|
-
const gitignored =
|
|
9929
|
+
const gitignored = _giMatch(fileRel); // 1.9.365 CV-6: gitignored 면 info 강등 (1.36.56 검수 #3: 캐시 사용)
|
|
9930
|
+
// 1.36.56 (F-06, 검수 #1 보정): 같은 "값 스팬"이 복수 패턴에 걸리면 1건만 — 컨텍스트 패턴(키에서 시작)과
|
|
9931
|
+
// 직접 패턴(값에서 시작)이 같은 토큰을 이중 보고하던 실측 케이스를 값 위치 기준으로 접는다. 서로 다른 토큰은 유지.
|
|
9932
|
+
const _seenAt = new Set();
|
|
9785
9933
|
for (const { name, re, valueGroup, requireSecretLike } of SECRET_PATTERNS) {
|
|
9786
9934
|
re.lastIndex = 0;
|
|
9787
9935
|
let m;
|
|
@@ -9793,6 +9941,11 @@ function _collectSecretFindings(root) {
|
|
|
9793
9941
|
if (_isPlaceholderSecret(val)) { if (re.lastIndex === m.index) re.lastIndex++; continue; }
|
|
9794
9942
|
if (valueGroup != null && requireSecretLike && !_looksSecretLike(val)) { if (re.lastIndex === m.index) re.lastIndex++; continue; }
|
|
9795
9943
|
const line = text.slice(0, m.index).split('\n').length;
|
|
9944
|
+
// 1.36.56 (F-06, 검수 #1): dedupe 키 = 값 스팬(시작+길이) — valueGroup 없으면 전체 매치 스팬
|
|
9945
|
+
const _valOff = (valueGroup != null && m[valueGroup]) ? m.index + m[0].indexOf(m[valueGroup]) : m.index;
|
|
9946
|
+
const _valKey = `${_valOff}:${String(val || '').length}`;
|
|
9947
|
+
if (_seenAt.has(_valKey)) { if (re.lastIndex === m.index) re.lastIndex++; continue; }
|
|
9948
|
+
_seenAt.add(_valKey);
|
|
9796
9949
|
findings.push({ file: fileRel, line, name, snippet: m[0].slice(0, 32), gitignored });
|
|
9797
9950
|
// 16th 버그헌트 F1: break 제거 — 같은 패턴이 한 파일에 여러 번(예: secret: + api_key: 둘 다 'Hardcoded password') 나오면 모두 보고(보안 FN 차단). zero-width 매치는 lastIndex 전진으로 무한루프 방지.
|
|
9798
9951
|
if (re.lastIndex === m.index) re.lastIndex++;
|
|
@@ -22607,6 +22760,10 @@ async function main() {
|
|
|
22607
22760
|
if (cmd === 'anchors') return anchorsCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd(), args[1] && !args[1].startsWith('-') ? args[1] : null); // 1.36.36: 정체성앵커 초안
|
|
22608
22761
|
if (cmd === 'toggle') return _tgl.toggleCmd(arg('--path', process.cwd()), args[1], args[2], args[3], { has, VERSION }); // 1.36.30: 기능 토글 (그래프 ⚙ 탭 연동)
|
|
22609
22762
|
if (cmd === 'tech') return _tech.techCmd(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd(), args[1], { has }); // 1.36.53 (UR-0062): 기술 프로필
|
|
22763
|
+
if (cmd === 'integrity') { // 1.36.57 (감사 F-04): managed 문서 무결성 점검/복구
|
|
22764
|
+
if (args[1] && args[1] !== 'check') { failJson(has('--json'), 'unknown_subcommand', `알 수 없는 integrity 하위명령: ${args[1]} (가능: check [--repair] [--force])`); return process.exit(process.exitCode || 1); }
|
|
22765
|
+
return integrityCmd(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd());
|
|
22766
|
+
}
|
|
22610
22767
|
// 1.36.51 (사용자 요청 UR-0061): 모호성 질문 + 미리보기 승인 워크플로
|
|
22611
22768
|
if (cmd === 'clarify') return _clar.clarifyCmd(arg('--path', process.cwd()), args.slice(1).filter(x => !x.startsWith('-')).join(' '), { has });
|
|
22612
22769
|
if (cmd === 'preview') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.57",
|
|
4
4
|
"description": "The AI-coding operations layer that makes \"done\" require evidence — persistent memory, evidence-gated completion checks, and clean handoffs for any AI agent (Claude Code, Codex, Cursor). State lives as plain files in your repo. CLI + MCP, 0 runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"leerness",
|