leerness 1.36.65 → 1.36.67
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 +19 -0
- package/README.md +4 -4
- package/bin/leerness.js +96 -34
- package/lib/drift.js +2 -0
- package/lib/io.js +3 -2
- package/lib/pure-utils.js +6 -2
- package/lib/session-close.js +647 -647
- package/lib/tech-profile.js +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.67 — 2026-07-22 — tech 갱신 시 그래프 동반 정합 (8차 헌트 F15)
|
|
4
|
+
|
|
5
|
+
- **F15**: `leerness tech` 가 프로필 변경을 감지하면 이미 존재하는 `leerness.html` 의 🛠 탭도 함께 갱신 — 종전엔 프로필만 최신이고 그래프는 옛 서비스를 계속 보여줬다(다음 handoff 전까지 불일치). 그래프 산출물이 없는 프로젝트는 무동작(생성 강제 안 함).
|
|
6
|
+
- **정직 이연 — F14(마감 보고의 미승인 미리보기)**: JSON 필드는 정상 동작했으나 텍스트 경로에서 출력 위치/스트림 상태가 맞지 않아 **반쪽 상태로 출하하지 않고 되돌림**. handoff 헤드라인 노출은 기존대로 유지되므로 기능 손실 없음. 다음 라운드에서 close 렌더 파이프라인 정비와 함께 재구현.
|
|
7
|
+
- 검증: 변경→그래프 동기화/무-그래프 프로젝트 무동작 실측, selftest 334.
|
|
8
|
+
|
|
9
|
+
## 1.36.66 — 2026-07-22 — codex 8차 홀리스틱 헌트: 기능 간 상호작용 11건 수정 (High 1·Med 8·Low 2)
|
|
10
|
+
|
|
11
|
+
라운드별 diff 검수가 못 본 "기능 조합" 결함을 교차 헌트(16 발견) — High/데이터손실/거짓판정 우선 11건 수정, 정보노출성 2건(F14/F15) 정직 이연.
|
|
12
|
+
|
|
13
|
+
- **F13 (write-skip × 손상바이트)**: writeUtf8 동일-내용 비교를 디코딩 텍스트→**바이트 동일**로 — 손상 바이트(FF)/BOM 파일에 유효 UTF-8 정화 write 가 억제되던 것.
|
|
14
|
+
- **F5 High (auto-update install × managed)**: /update.md 를 raw writeUtf8 로 덮어써 커스텀 파괴 → writeIfSafe mergeManaged 경로로.
|
|
15
|
+
- **F6 (update --force 미전달)** → install 에 force 전달. **F4 (drift --json critical exit 0)** → exit 1 동치.
|
|
16
|
+
- **F2/F3 거짓판정 룰**: "version and changelog" 복합 룰이 version 절만 보고 pass(F2), "tests must **pass**" 가 실패 증거도 흔적만 있으면 pass(F3) → 절별 독립 평가 후 AND 집계 + 오늘자 증거 exit0 파싱.
|
|
17
|
+
- **F7/F8 (손상 manifest × 언어)**: manifest parse throw 가 유효 LANGUAGE 폴백까지 건너뛰어 en→ko 회귀 → 소스별 개별 try. integrity 도 detectLanguageValue 공용.
|
|
18
|
+
- **F10 (integrity repair × 커스텀)**: h1_lost/truncated 는 managed 병합 복구(커스텀 안전규칙 보존), 부재/판독불가만 raw. **F11**: en 프로젝트 anchors 초안 영어.
|
|
19
|
+
- 이연(정직): F14(session close 에 pending preview 노출)·F15(tech --json 후 graph 재생성) — 정보 노출성, 데이터 보존됨. F9(초고령 KO 템플릿 provenance)·F12(skills-lock 명명)은 설계 판단 필요.
|
|
20
|
+
- 검증: F13/F7/F11 행위 + F4/F5/F6/F8/F10 소스가드 selftest 334, 개별 실측(F2 version-only→pending 등), e2e.
|
|
21
|
+
|
|
3
22
|
## 1.36.65 — 2026-07-22 — no-op 재설치 + 동일-내용 쓰기 스킵 (외부감사 F-08) — R-0001 검수 10회전
|
|
4
23
|
|
|
5
24
|
무변경 재init 의 백업/아카이브/타임스탬프 증식(감사 지적) 해소 — 거짓 no-op 은 검수가 차단.
|
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.67 하네스를 사용합니다. 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.67는 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.65는 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.67 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
|
|
201
201
|
|
|
202
202
|
### 성능 가이드
|
|
203
203
|
|
|
@@ -235,5 +235,5 @@ 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.67: 2026-07-22
|
|
239
239
|
<!-- leerness:project-readme:end -->
|
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.67';
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -243,12 +243,16 @@ function detectLanguageValue(root, value = 'auto') {
|
|
|
243
243
|
// 1.36.52 (외부 GPT 감사 F-01, 재현확정): 이미 초기화된 프로젝트는 저장된 명시 언어를 최우선 —
|
|
244
244
|
// 1회차 init 이 생성한 한국어 관리 블록을 2회차 콘텐츠 감지가 오인해 en→ko 로 뒤집던 멱등성 버그.
|
|
245
245
|
// 콘텐츠 자동 감지는 신규 프로젝트에만. (감사 첨부 패치 검증 후 채택)
|
|
246
|
+
// 1.36.66 (8차 헌트 F7): 소스별 개별 try — 손상 manifest 의 parse throw 가 유효한 LANGUAGE 폴백까지 건너뛰게 하던
|
|
247
|
+
// 것 차단(en 프로젝트가 manifest 손상 시 콘텐츠 휴리스틱으로 ko 회귀). manifest → LANGUAGE → 콘텐츠 순.
|
|
246
248
|
try {
|
|
247
249
|
const mf = path.join(root, '.harness', 'manifest.json');
|
|
248
250
|
if (exists(mf)) {
|
|
249
251
|
const saved = String((JSON.parse(read(mf)) || {}).language || '').toLowerCase();
|
|
250
252
|
if (saved === 'ko' || saved === 'en') return saved;
|
|
251
253
|
}
|
|
254
|
+
} catch {}
|
|
255
|
+
try {
|
|
252
256
|
const lf = path.join(root, '.harness', 'LANGUAGE');
|
|
253
257
|
if (exists(lf)) {
|
|
254
258
|
const saved = String(read(lf) || '').trim().toLowerCase();
|
|
@@ -5434,6 +5438,35 @@ function _selfTestCases() {
|
|
|
5434
5438
|
const wired = pkg.scripts.lint === 'node ./scripts/lint.js' && ['test', 'test:fast', 'test:core'].every(k => pkg.scripts[k].startsWith('node ./scripts/lint.js &&')); // (검수 #1) test:core 포함
|
|
5435
5439
|
return cleanOk && caught && synCaught && bomCaught && jsonCaught && wired;
|
|
5436
5440
|
} finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
5441
|
+
} },
|
|
5442
|
+
{ name: 'codex 8차 홀리스틱 헌트 (1.36.66): writeUtf8 바이트비교(F13)·detectLang 소스별 try(F7)·en anchors(F11)·drift --json exit(F4)·integrity managed복구(F10) — 행위+소스가드', run: () => {
|
|
5443
|
+
const io = require('../lib/io'); const pu = require('../lib/pure-utils');
|
|
5444
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_h66_'));
|
|
5445
|
+
try {
|
|
5446
|
+
// F13: 손상 바이트(FF) 파일에 유효 UTF-8 쓰면 바이트 다르므로 정화 write 수행
|
|
5447
|
+
const f13 = path.join(tmp, 'c.txt');
|
|
5448
|
+
fs.writeFileSync(f13, Buffer.from([0x7b, 0xff, 0x7d]));
|
|
5449
|
+
io.writeUtf8(f13, '{"ok":true}');
|
|
5450
|
+
const f13ok = fs.readFileSync(f13, 'utf8') === '{"ok":true}' && !fs.readFileSync(f13).includes(0xff);
|
|
5451
|
+
// 동일 바이트는 스킵(멱등) — mtime 안정
|
|
5452
|
+
const before = fs.statSync(f13).mtimeMs; io.writeUtf8(f13, '{"ok":true}'); const skipOk = fs.statSync(f13).mtimeMs === before;
|
|
5453
|
+
// F7: 손상 manifest + 유효 LANGUAGE → en 유지 (detectLanguageValue)
|
|
5454
|
+
fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true });
|
|
5455
|
+
fs.writeFileSync(path.join(tmp, '.harness', 'manifest.json'), '{broken');
|
|
5456
|
+
fs.writeFileSync(path.join(tmp, '.harness', 'LANGUAGE'), 'en\n');
|
|
5457
|
+
const f7ok = detectLanguageValue(tmp, 'auto') === 'en';
|
|
5458
|
+
// F11: en anchors 초안 영어
|
|
5459
|
+
const d = pu._draftAnchors({ pkgDescription: 'payment service', lang: 'en' });
|
|
5460
|
+
const f11ok = !/[가-힣]/.test([...d.purpose, ...d.goal].join('\n')) && d.goal.some(l => l.includes('Keep the product'));
|
|
5461
|
+
// 소스가드: F4/F5/F6/F8/F10
|
|
5462
|
+
const dr = read(path.join(__dirname, '..', 'lib', 'drift.js'));
|
|
5463
|
+
const s = read(__filename);
|
|
5464
|
+
const guards = /if \(level === '🔴 critical'\) process\.exitCode = 1;\s*\n\s*log\(JSON\.stringify/.test(dr)
|
|
5465
|
+
&& s.includes("writeIfSafe(root, '.claude/commands/update.md', _updContent")
|
|
5466
|
+
&& s.includes('force: !!opts.force, dry: false, migration: true')
|
|
5467
|
+
&& s.includes('const lang = detectLanguageValue(root, ') && s.includes("managedMerge(key, tmpl, cur, null, { lang })");
|
|
5468
|
+
return f13ok && skipOk && f7ok && f11ok && guards;
|
|
5469
|
+
} finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
5437
5470
|
} }
|
|
5438
5471
|
];
|
|
5439
5472
|
}
|
|
@@ -5785,7 +5818,7 @@ function anchorsCmd(root, sub) {
|
|
|
5785
5818
|
try { if (exists(path.join(root, 'README.md'))) readmeText = read(path.join(root, 'README.md')).slice(0, 4000); } catch {}
|
|
5786
5819
|
let milestones = [], tasks = [];
|
|
5787
5820
|
try { const rd = _roadmapData(root); milestones = rd.milestones || []; tasks = rd.tasks || []; } catch {}
|
|
5788
|
-
const draft = _draftAnchors({ pkgDescription, readmeText, milestones, tasks });
|
|
5821
|
+
const draft = _draftAnchors({ pkgDescription, readmeText, milestones, tasks, lang: _uiLang(root) }); // 1.36.66 (F11): 초안 언어
|
|
5789
5822
|
if (!draft.hasSignal) { failJson(json, 'no_signal', '초안을 만들 실신호가 없음 (package.json description / README 산문 / milestone / 활성 task 모두 부재) — 직접 작성 필요'); return; }
|
|
5790
5823
|
const apply = has('--apply');
|
|
5791
5824
|
const applied = [];
|
|
@@ -5822,8 +5855,11 @@ function integrityCmd(root) {
|
|
|
5822
5855
|
const json = has('--json');
|
|
5823
5856
|
const repair = has('--repair');
|
|
5824
5857
|
if (!exists(path.join(root, '.harness'))) { failJson(json, 'harness_missing', `leerness 미설치: ${root} — 먼저 leerness init`); return; }
|
|
5825
|
-
let
|
|
5826
|
-
try {
|
|
5858
|
+
let minimal = false;
|
|
5859
|
+
try { minimal = !!JSON.parse(read(path.join(root, '.harness', 'manifest.json'))).minimal; } catch {}
|
|
5860
|
+
// 1.36.66 (8차 헌트 F8): 언어는 init/migrate 와 동일 해석기 사용 — 손상 manifest 여도 LANGUAGE 폴백으로 en 유지
|
|
5861
|
+
// (종전엔 manifest 만 봐서 손상 시 ko 로 복구 → en 프로젝트에 한국어 템플릿 재생성).
|
|
5862
|
+
const lang = detectLanguageValue(root, 'auto');
|
|
5827
5863
|
// (검수 #1) minimal 설치 존중 — minimal 이 의도적으로 뺀 문서를 '부재 손상'으로 오판/재생성하지 않는다
|
|
5828
5864
|
const templates = coreFiles(root, lang, [], { minimal });
|
|
5829
5865
|
const docs = _INTEGRITY_DOCS.filter(k => typeof templates[k] === 'string');
|
|
@@ -5855,6 +5891,7 @@ function integrityCmd(root) {
|
|
|
5855
5891
|
const f = { file: key, kind, advisory, templateBytes: Buffer.byteLength(tmpl, 'utf8') };
|
|
5856
5892
|
if (repair && !advisory) {
|
|
5857
5893
|
try {
|
|
5894
|
+
const _readable = kind === 'h1_lost' || kind === 'truncated';
|
|
5858
5895
|
if (exists(fp)) {
|
|
5859
5896
|
const bakDir = path.join(root, '.harness', 'archive');
|
|
5860
5897
|
mkdirp(bakDir);
|
|
@@ -5862,7 +5899,14 @@ function integrityCmd(root) {
|
|
|
5862
5899
|
fs.copyFileSync(fp, path.join(bakDir, `integrity-${path.basename(key)}-${Date.now()}-${process.pid}-${++_integSeq}.bak`));
|
|
5863
5900
|
f.backedUp = true;
|
|
5864
5901
|
}
|
|
5865
|
-
|
|
5902
|
+
// 1.36.66 (8차 헌트 F10): 읽을 수 있는 손상(h1_lost/truncated)은 managed 병합으로 복구 —
|
|
5903
|
+
// raw 교체는 사용자가 추가한 커스텀 안전 규칙까지 지웠다. 부재/판독불가만 raw 재생성.
|
|
5904
|
+
if (_readable && exists(fp)) {
|
|
5905
|
+
let cur = ''; try { cur = read(fp); } catch {}
|
|
5906
|
+
writeUtf8(fp, managedMerge(key, tmpl, cur, null, { lang }));
|
|
5907
|
+
} else {
|
|
5908
|
+
writeUtf8(fp, tmpl);
|
|
5909
|
+
}
|
|
5866
5910
|
f.repaired = true; repaired.push(key);
|
|
5867
5911
|
} catch (e) { f.repaired = false; f.error = String(e.message || e).slice(0, 120); repairFailed = true; }
|
|
5868
5912
|
}
|
|
@@ -16453,36 +16497,51 @@ function verifyRules(root) {
|
|
|
16453
16497
|
const ev = exists(evidencePath(root)) ? read(evidencePath(root)) : '';
|
|
16454
16498
|
const todayStr = today();
|
|
16455
16499
|
const results = [];
|
|
16500
|
+
// 1.36.66 (8차 헌트 F2/F3): 절별 독립 평가 후 AND 집계 — 종전 if/else-if 는 "version and changelog" 규칙에서
|
|
16501
|
+
// version 절만 보고 pass 했고(F2), "tests must pass" 는 실패한 테스트 증거(exit≠0)도 흔적만 있으면 pass 했다(F3).
|
|
16502
|
+
const _todayEvidenceExit0 = () => {
|
|
16503
|
+
// 오늘자 최신 verify-code/test 증거 블록에서 실패 신호(exit=null/nonzero, FAIL)가 없는지
|
|
16504
|
+
const blocks = ev.split(/\n(?=## \d{4}-\d{2}-\d{2})/).filter(b => new RegExp(`^## ${todayStr}`).test(b.trim()) && /verify-code|test/i.test(b));
|
|
16505
|
+
if (!blocks.length) return null; // 흔적 없음
|
|
16506
|
+
const last = blocks[blocks.length - 1];
|
|
16507
|
+
if (/exit\s*=\s*(?!0\b)(\d+|null)/i.test(last) || /\bFAIL\b/.test(last)) return false;
|
|
16508
|
+
return true;
|
|
16509
|
+
};
|
|
16456
16510
|
for (const r of active) {
|
|
16457
|
-
let verified = 'manual';
|
|
16458
|
-
let note = '';
|
|
16459
16511
|
const rl = r.rule.toLowerCase();
|
|
16512
|
+
const clauses = []; // { kind, verified }
|
|
16460
16513
|
if (/version|버전|bump|상승/i.test(rl)) {
|
|
16461
|
-
if (prev.packageVersion && cur.packageVersion && prev.packageVersion !== cur.packageVersion) {
|
|
16462
|
-
|
|
16463
|
-
|
|
16464
|
-
|
|
16465
|
-
|
|
16466
|
-
|
|
16467
|
-
}
|
|
16468
|
-
|
|
16469
|
-
|
|
16470
|
-
|
|
16471
|
-
|
|
16472
|
-
|
|
16514
|
+
if (prev.packageVersion && cur.packageVersion && prev.packageVersion !== cur.packageVersion) clauses.push({ kind: 'version', verified: 'pass', note: `${prev.packageVersion} → ${cur.packageVersion}` });
|
|
16515
|
+
else if (!prev.packageVersion) clauses.push({ kind: 'version', verified: 'baseline', note: `초기 ${cur.packageVersion || '미확인'}` });
|
|
16516
|
+
else clauses.push({ kind: 'version', verified: 'pending', note: '버전 변경 없음' });
|
|
16517
|
+
}
|
|
16518
|
+
if (/changelog|패치노트|patch.*note|note.*추가|note.*add/i.test(rl)) {
|
|
16519
|
+
if (prev.changelogMtime && cur.changelogMtime && cur.changelogMtime > prev.changelogMtime) clauses.push({ kind: 'changelog', verified: 'pass', note: 'CHANGELOG.md 갱신 감지' });
|
|
16520
|
+
else if (!prev.changelogMtime) clauses.push({ kind: 'changelog', verified: 'baseline', note: '초기 측정' });
|
|
16521
|
+
else clauses.push({ kind: 'changelog', verified: 'pending', note: 'CHANGELOG.md 변경 없음' });
|
|
16522
|
+
}
|
|
16523
|
+
if (/test|테스트|verify/i.test(rl)) {
|
|
16524
|
+
// "must pass" 의미면 exit0 확인, 아니면 "흔적 존재"(must run)
|
|
16525
|
+
const mustPass = /pass|통과|성공|green/i.test(rl);
|
|
16526
|
+
if (mustPass) {
|
|
16527
|
+
const ok0 = _todayEvidenceExit0();
|
|
16528
|
+
clauses.push({ kind: 'test', verified: ok0 === true ? 'pass' : 'pending', note: ok0 === true ? '오늘 테스트 통과(exit0)' : ok0 === false ? '오늘 테스트 실패(exit≠0)' : '오늘 테스트 흔적 없음' });
|
|
16473
16529
|
} else {
|
|
16474
|
-
|
|
16530
|
+
const hasTest = new RegExp(`## ${todayStr}.*verify-code|## ${todayStr}.*test`, 'i').test(ev);
|
|
16531
|
+
clauses.push({ kind: 'test', verified: hasTest ? 'pass' : 'pending', note: hasTest ? '오늘 verify-code 흔적' : '오늘 verify-code 호출 없음' });
|
|
16475
16532
|
}
|
|
16476
|
-
} else if (/test|테스트|verify/i.test(rl)) {
|
|
16477
|
-
const hasTest = new RegExp(`## ${todayStr}.*verify-code|## ${todayStr}.*test`, 'i').test(ev);
|
|
16478
|
-
verified = hasTest ? 'pass' : 'pending';
|
|
16479
|
-
note = hasTest ? '오늘 verify-code 흔적' : '오늘 verify-code 호출 없음';
|
|
16480
|
-
} else if (/deploy|배포|publish|push|release/i.test(rl)) {
|
|
16481
|
-
verified = 'manual'; note = '배포는 사용자 명시 호출 (leerness release publish)';
|
|
16482
|
-
} else {
|
|
16483
|
-
verified = 'manual'; note = '자동 검증 패턴 없음 — 수동 확인';
|
|
16484
16533
|
}
|
|
16485
|
-
|
|
16534
|
+
if (/deploy|배포|publish|push|release/i.test(rl) && !clauses.length) clauses.push({ kind: 'deploy', verified: 'manual', note: '배포는 사용자 명시 호출 (leerness release publish)' });
|
|
16535
|
+
let verified, note;
|
|
16536
|
+
if (!clauses.length) { verified = 'manual'; note = '자동 검증 패턴 없음 — 수동 확인'; }
|
|
16537
|
+
else {
|
|
16538
|
+
// AND 집계: 하나라도 pending 이면 pending, 모두 pass/baseline 이면 (pass 있으면 pass, 아니면 baseline)
|
|
16539
|
+
if (clauses.some(c => c.verified === 'pending')) verified = 'pending';
|
|
16540
|
+
else if (clauses.some(c => c.verified === 'pass')) verified = 'pass';
|
|
16541
|
+
else verified = clauses.some(c => c.verified === 'manual') ? 'manual' : 'baseline';
|
|
16542
|
+
note = clauses.map(c => `${c.kind}:${c.note}`).join(' · ');
|
|
16543
|
+
}
|
|
16544
|
+
results.push({ ...r, verified, note, clauses: clauses.length > 1 ? clauses.map(c => ({ kind: c.kind, verified: c.verified })) : undefined });
|
|
16486
16545
|
}
|
|
16487
16546
|
// lastVerified 갱신 (pass인 경우만)
|
|
16488
16547
|
for (const r of rules) {
|
|
@@ -17691,7 +17750,7 @@ async function updateCmd(root, opts = {}) {
|
|
|
17691
17750
|
try { const _hv = read(path.join(root, '.harness', 'HARNESS_VERSION')).trim(); if (_hv) nextLeerness = _hv; } catch {}
|
|
17692
17751
|
} else {
|
|
17693
17752
|
log(`\nRunning in-process migrate (already on latest ${VERSION})…`);
|
|
17694
|
-
await install(root, { force:
|
|
17753
|
+
await install(root, { force: !!opts.force, dry: false, migration: true, nonInteractive: true }); // 1.36.66 (8차 헌트 F6): --force 전달 — no-op 우회해 손상 템플릿 재작성
|
|
17695
17754
|
}
|
|
17696
17755
|
log('\n# Post-migration checks');
|
|
17697
17756
|
status(root);
|
|
@@ -17733,10 +17792,12 @@ function autoUpdateInstall(root) {
|
|
|
17733
17792
|
}
|
|
17734
17793
|
writeUtf8(settingsFile, JSON.stringify(settings, null, 2) + '\n');
|
|
17735
17794
|
// 1.36.62 (검수 #1): en 프로젝트의 /update 를 한국어로 되덮던 것 — 언어 인지 렌더
|
|
17736
|
-
|
|
17737
|
-
|
|
17795
|
+
// 1.36.66 (8차 헌트 F5): raw writeUtf8 은 managed 파일 커스텀을 파괴했다 — coreFiles 와 동일 병합 경로(writeIfSafe mergeManaged)로.
|
|
17796
|
+
const _updLang = (() => { try { return read(path.join(root, '.harness', 'LANGUAGE')).trim().toLowerCase() === 'en' ? 'en' : 'ko'; } catch { return 'ko'; } })();
|
|
17797
|
+
const _updContent = _updLang === 'en'
|
|
17738
17798
|
? `# /update\n\nRun the leerness auto-update (detect → migrate → verify).\n\n\`\`\`\n!leerness update --yes\n\`\`\`\n\nCheck only:\n\n\`\`\`\n!leerness update --check\n\`\`\`\n`
|
|
17739
|
-
: `# /update\n\nleerness 자동 업데이트 (감지 → 마이그레이션 → 검증).\n\n\`\`\`\n!leerness update --yes\n\`\`\`\n\n체크만:\n\n\`\`\`\n!leerness update --check\n\`\`\`\n
|
|
17799
|
+
: `# /update\n\nleerness 자동 업데이트 (감지 → 마이그레이션 → 검증).\n\n\`\`\`\n!leerness update --yes\n\`\`\`\n\n체크만:\n\n\`\`\`\n!leerness update --check\n\`\`\`\n`;
|
|
17800
|
+
writeIfSafe(root, '.claude/commands/update.md', _updContent, { mergeManaged: true, lang: _updLang });
|
|
17740
17801
|
ok('auto-update SessionStart hook installed (.claude/settings.local.json)');
|
|
17741
17802
|
if (removedLegacy) ok(`legacy hook 제거: ${removedLegacy}건 (leerness-plus → leerness 통합)`);
|
|
17742
17803
|
if (upgradedQuiet) ok(`hook 업그레이드: ${upgradedQuiet}건 → --quiet (up-to-date 시 무음, 1.9.364)`);
|
|
@@ -23064,7 +23125,8 @@ async function main() {
|
|
|
23064
23125
|
if (cmd === 'enforce') return enforceCmd(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd(), args[1]); // 1.36.43: 사용 강제 (git pre-commit)
|
|
23065
23126
|
if (cmd === 'anchors') return anchorsCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd(), args[1] && !args[1].startsWith('-') ? args[1] : null); // 1.36.36: 정체성앵커 초안
|
|
23066
23127
|
if (cmd === 'toggle') return _tgl.toggleCmd(arg('--path', process.cwd()), args[1], args[2], args[3], { has, VERSION }); // 1.36.30: 기능 토글 (그래프 ⚙ 탭 연동)
|
|
23067
|
-
|
|
23128
|
+
// 1.36.53 (UR-0062): 기술 프로필 · 1.36.67 (F15): 변경 시 기존 leerness.html 동반 갱신(있을 때만)
|
|
23129
|
+
if (cmd === 'tech') return _tech.techCmd(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd(), args[1], { has, regenGraph: (r) => { if (exists(path.join(r, 'leerness.html'))) _graph.graphHtmlCmd(r, { _roadmapData, _loadDecisions, _loadLessons, _parseFeatureGraph, _loadToggles: _tgl.loadToggles, _toggleRegistry: _tgl.TOGGLE_REGISTRY, _loadTechProfile: _tech.loadTechProfile, quiet: true }); } });
|
|
23068
23130
|
if (cmd === 'integrity') { // 1.36.57 (감사 F-04): managed 문서 무결성 점검/복구
|
|
23069
23131
|
if (args[1] && args[1] !== 'check') { failJson(has('--json'), 'unknown_subcommand', `알 수 없는 integrity 하위명령: ${args[1]} (가능: check [--repair] [--force])`); return process.exit(process.exitCode || 1); }
|
|
23070
23132
|
return integrityCmd(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd());
|
package/lib/drift.js
CHANGED
|
@@ -313,6 +313,8 @@ function driftCheckCmd(root, opts = {}, deps = {}) {
|
|
|
313
313
|
}
|
|
314
314
|
}
|
|
315
315
|
if (has('--json')) {
|
|
316
|
+
// 1.36.66 (8차 헌트 F4): --json 도 사람용과 exit 동치 — critical 은 exit 1 (조기 return 전에 설정)
|
|
317
|
+
if (level === '🔴 critical') process.exitCode = 1;
|
|
316
318
|
log(JSON.stringify({ root, score: totalScore, level, signals, fired, appsZeroTask }, null, 2));
|
|
317
319
|
return;
|
|
318
320
|
}
|
package/lib/io.js
CHANGED
|
@@ -112,8 +112,9 @@ function _rescueCorruptJson(p, s) {
|
|
|
112
112
|
}
|
|
113
113
|
function writeUtf8(p, s) {
|
|
114
114
|
// 1.36.65 (외부감사 F-08): 동일-내용 재기록 스킵 — 무변경 재설치/조정기의 mtime·diff churn 을 전역 해소.
|
|
115
|
-
//
|
|
116
|
-
|
|
115
|
+
// 1.36.66 (8차 헌트 F13): 비교는 디코딩 텍스트가 아니라 "바이트 동일"로 — 손상 바이트(FF)/BOM 파일에
|
|
116
|
+
// 유효 UTF-8 을 쓸 때 read() 의 BOM-strip·치환문자가 텍스트를 같게 보이게 해 정화 write 가 억제됐다.
|
|
117
|
+
try { if (fs.existsSync(p) && fs.readFileSync(p).equals(Buffer.from(String(s), 'utf8'))) return; } catch {}
|
|
117
118
|
mkdirp(path.dirname(p));
|
|
118
119
|
_rescueCorruptJson(p, s);
|
|
119
120
|
const tmp = `${p}.tmp-${process.pid}-${++_writeSeq}`;
|
package/lib/pure-utils.js
CHANGED
|
@@ -1201,10 +1201,14 @@ function _draftAnchors(sig = {}) {
|
|
|
1201
1201
|
if (readmeLead && readmeLead !== desc) purpose.push(`- ${_cutAt(readmeLead, 200)}`);
|
|
1202
1202
|
const ms = (sig.milestones || []).filter(m => m && m.title).slice(0, 3);
|
|
1203
1203
|
const activeTasks = (sig.tasks || []).filter(t => t && /in-progress|planned|requested/.test(String(t.status || ''))).slice(0, 3);
|
|
1204
|
-
|
|
1204
|
+
// 1.36.66 (8차 헌트 F11): en 프로젝트는 초안 마커·기본 goal 도 영어
|
|
1205
|
+
const _en = String(sig.lang || '').toLowerCase() === 'en';
|
|
1206
|
+
if (desc || readmeLead) goal.push(_en ? `- Keep the product working and evolving it: ${_cutAt(desc || readmeLead, 140)}` : `- 제품을 동작 상태로 유지·발전시킨다: ${_cutAt(desc || readmeLead, 140)}`);
|
|
1205
1207
|
for (const m of ms) goal.push(`- [${m.id || 'M'}] ${_oneLine(m.title).slice(0, 120)}${m.status ? ` (${m.status})` : ''}`);
|
|
1206
1208
|
if (!goal.length && activeTasks.length) for (const t of activeTasks) goal.push(`- ${t.id}: ${_oneLine(t.request).slice(0, 100)}`);
|
|
1207
|
-
const marker =
|
|
1209
|
+
const marker = _en
|
|
1210
|
+
? '<!-- draft: leerness anchors — draft from real signals. Remove this comment after AI/user review to confirm -->'
|
|
1211
|
+
: '<!-- draft: leerness anchors — 실신호 기반 초안. AI/사용자가 검토·수정 후 이 주석을 지우면 확정 -->';
|
|
1208
1212
|
return {
|
|
1209
1213
|
hasSignal: purpose.length > 0 || goal.length > 0,
|
|
1210
1214
|
purpose: purpose.length ? [marker, ...purpose] : [],
|