leerness 1.36.175 → 1.36.177
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 +8 -0
- package/README.md +4 -4
- package/bin/leerness.js +47 -77
- package/lib/agents.js +5 -1
- package/lib/drift.js +525 -360
- package/lib/health.js +7 -4
- package/lib/io.js +242 -1
- package/lib/session-close.js +47 -16
- package/lib/shell-encoding.js +175 -0
- package/package.json +4 -4
- package/scripts/e2e-core.js +14 -3
- package/scripts/e2e.js +14 -3
- package/scripts/mutation-integrity-probe.js +536 -0
- package/scripts/release-runtime-probe.js +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.177 — 2026-08-29
|
|
4
|
+
|
|
5
|
+
- T-0022 데이터 무결성: env/drift/session BOM 수정을 공용 CAS·Windows ReplaceFile 경계로 통합하고 CP949·모호 인코딩·손상 BOM·메타데이터·동시 writer/ADS·부분진행 복구를 실패-폐쇄 처리. auto-fix 전 단계 JSON/exit 정직화, 스캔 I/O 오류 및 unborn/실행불가 Git 구분, OS 매트릭스 mutation-integrity 회귀 추가.
|
|
6
|
+
|
|
7
|
+
## 1.36.176 — 2026-08-29
|
|
8
|
+
|
|
9
|
+
- T-0089: agents bench가 활성화되지 않은 provider의 버전 명령까지 실행하던 환경 의존성을 제거했습니다. bench 후보를 opt-in provider로 먼저 제한하고, E2E는 레지스트리의 10개 플래그를 모두 비활성화한 뒤 PATH 선두 실행 마커가 생성되지 않는지 검증합니다. 수정 후 동일 Windows Node 26 환경 직접 프로브 3회는 394/362/342ms(최대 394ms)였으며, 기존 15초 제한은 최댓값 대비 약 38.1배 여유라 불필요한 상향을 하지 않습니다.
|
|
10
|
+
|
|
3
11
|
## 1.36.175 — 2026-08-29
|
|
4
12
|
|
|
5
13
|
- T-0095 false-claim 감사를 199개 회귀 프로브로 고정했습니다. round-history와 release cadence는 CLI·프로젝트 하네스·저장소 태그 버전 및 하네스 판독 상태의 출처를 분리하고 기존 currentVersion 별칭에 leerness_cli 범위를 명시합니다. observation-only로 명시 분류한 `about`·`identity`·`status`·`commands`·`install-safety`·`capabilities`·`round-history`·`milestones`·`pulse`·`release cadence`와 MCP 서버 시작은 레거시 `.harness/`를 마이그레이션하거나 사용량 텔레메트리를 쓰지 않습니다. 읽기 전용 MCP 도구도 서버의 우연한 cwd와 명시 대상을 보존합니다. 이 계약은 상위 프로세스의 `LEERNESS_INTERNAL`까지 명시적으로 제거한 격리 환경에서 canonical 대상의 실제 usage 파일로 검증하며, 쓰기 MCP 호출은 대상 프로젝트에만 계속 귀속됩니다. `status`는 선택된 `.harness/` 또는 `.leerness/`를 한 번만 해석해 실제 버전·누락 경로·`workspaceDir`을 보고합니다. `.harness/`·`.leerness/` 충돌이나 잘못된 workspace override는 `unreadable` 성공으로 삼키지 않고 정확한 `workspace_dir_*` 오류로 종료하며, pulse/handoff 집계는 workspace 출처 오류를 구조화해 노출하면서 독립적으로 측정한 Git 라운드 수를 보존합니다. Git 누락·spawn 예외·timeout·signal·nonzero exit이면 round-history·milestones·release cadence는 `git_history_unavailable`로 실패하고 pulse/handoff/health/session-close는 `roundCount:null`, milestone의 `totalRounds/reachedCount/reached/avgRoundsPerDay:null`, `recentChanges:null`과 `recentChangesState/recentChangesError`를 보존해 R0/빈 이력으로 위장하지 않습니다. Git 측정 성공 시에는 기존 `recentChanges` 배열 계약을 그대로 유지하고, `latestTags[].date`의 전체 타임스탬프 계약도 보존하면서 별도 `dateOnly`를 제공합니다. 예외·timeout·signal·nonzero 네 시나리오는 일곱 공개 표면 전부에서 각각 실행합니다. cadence는 `--path > positional > cwd` 단일 우선순위를 검증과 실행에 함께 쓰고, 태그 간격이 없으면 healthy가 아니라 `insufficient-data`로 판정을 보류합니다. E2E/적대 프로브의 Git 픽스처도 공용 `gitSpawn`을 사용해 상위 셸의 외부 저장소·index·일회성 config 주입을 제거하며, persistent global/system Git config도 빈 임시 파일로 격리해 외부 hooksPath가 픽스처에서 실행되지 않게 합니다.
|
package/README.md
CHANGED
|
@@ -138,7 +138,7 @@ MIT
|
|
|
138
138
|
<!-- leerness:project-readme:start -->
|
|
139
139
|
## Leerness Project Harness
|
|
140
140
|
|
|
141
|
-
이 프로젝트는 Leerness v1.36.
|
|
141
|
+
이 프로젝트는 Leerness v1.36.177 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
142
142
|
|
|
143
143
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
144
144
|
|
|
@@ -192,7 +192,7 @@ leerness memory restore decision <date|title>
|
|
|
192
192
|
|
|
193
193
|
### MCP server (외부 AI 통합)
|
|
194
194
|
|
|
195
|
-
Leerness v1.36.
|
|
195
|
+
Leerness v1.36.177는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **89개 도구**를 노출:
|
|
196
196
|
|
|
197
197
|
```jsonc
|
|
198
198
|
// 카테고리별
|
|
@@ -213,7 +213,7 @@ Leerness v1.36.175는 stdio JSON-RPC MCP server를 내장합니다 — Claude Co
|
|
|
213
213
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
214
214
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) 회귀 테스트 갱신 → 4) 전체 e2e 스위트 통과 → 5) npm publish + git tag → 6) main push → 7) session close → 8) 다음 라운드 예약.
|
|
215
215
|
|
|
216
|
-
현재 누적: **v1.9.x → 1.36.
|
|
216
|
+
현재 누적: **v1.9.x → 1.36.177 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
|
|
217
217
|
|
|
218
218
|
### 성능 가이드
|
|
219
219
|
|
|
@@ -251,5 +251,5 @@ leerness release pack --close --auto-main-push
|
|
|
251
251
|
- `.leerness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
252
252
|
- `.leerness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
253
253
|
|
|
254
|
-
Last synced by Leerness v1.36.
|
|
254
|
+
Last synced by Leerness v1.36.177: 2026-08-29
|
|
255
255
|
<!-- leerness:project-readme:end -->
|
package/bin/leerness.js
CHANGED
|
@@ -7,6 +7,7 @@ const cp = require('child_process');
|
|
|
7
7
|
const { log, ok, warn, fail, failJson, setQuiet, today, now, absRoot, exists, read, readBuf, mkdirp, writeUtf8, append, rel, setDryRunGuard, mkdirpRaw } = require('../lib/io'); // 1.9.382/383 (UR-0025): 출력/시간/파일 프리미티브 공유 모듈 · 1.10.2 (UR-0146): setQuiet
|
|
8
8
|
const { resolveNpmCliPath, spawnNpmSync } = require('../lib/npm-process');
|
|
9
9
|
const { resolvePortableLaunch, spawnPortable, spawnPortableSync } = require('../lib/portable-process');
|
|
10
|
+
const { scanShellScriptsEncoding, applyShellScriptUtf8Bom } = require('../lib/shell-encoding');
|
|
10
11
|
const os = require('os'); // 1.9.178: _publishToNpm 에서 os.tmpdir() 사용 (전역 import)
|
|
11
12
|
const readline = require('readline');
|
|
12
13
|
// 1.9.274 (UR-0025 1단계): 순수 유틸 함수 모듈 분리 (require-based, 비파괴). selftest 7종이 동작 검증.
|
|
@@ -48,7 +49,7 @@ const {
|
|
|
48
49
|
migrateLegacyWorkspace,
|
|
49
50
|
} = require('../lib/workspace-dir');
|
|
50
51
|
|
|
51
|
-
const VERSION = '1.36.
|
|
52
|
+
const VERSION = '1.36.177';
|
|
52
53
|
|
|
53
54
|
// MCP lifecycle 주소 표식은 현재 CLI 호출 한 번에만 유효하다. CLI bootstrap에서 즉시 env에서
|
|
54
55
|
// 떼어 두어 `--no-record`/hook처럼 presence 기록 함수에 도달하지 않는 경로도 후속 child에 유출하지 않는다.
|
|
@@ -3798,52 +3799,7 @@ function _terminalEncodingNotice(opts = {}) {
|
|
|
3798
3799
|
// 셸 스크립트 (.ps1/.bat/.cmd/.sh) 인코딩 위험 감지
|
|
3799
3800
|
// 위험 시그널: 한국어 (또는 비-ASCII) 문자 + BOM 없음 + Windows 환경
|
|
3800
3801
|
function _scanShellScriptsEncoding(root) {
|
|
3801
|
-
|
|
3802
|
-
function walk(dir, depth = 0) {
|
|
3803
|
-
if (depth > 3) return; // 너무 깊이 들어가지 않음
|
|
3804
|
-
let entries;
|
|
3805
|
-
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
3806
|
-
for (const e of entries) {
|
|
3807
|
-
const fp = path.join(dir, e.name);
|
|
3808
|
-
if (e.isDirectory()) {
|
|
3809
|
-
if (/^(node_modules|\.git|__pycache__|venv|\.venv|dist|build)$/.test(e.name)) continue;
|
|
3810
|
-
walk(fp, depth + 1);
|
|
3811
|
-
} else if (e.isFile() && /\.(ps1|bat|cmd|sh)$/i.test(e.name)) {
|
|
3812
|
-
result.scanned++;
|
|
3813
|
-
try {
|
|
3814
|
-
const buf = fs.readFileSync(fp);
|
|
3815
|
-
const hasBOM = buf.length >= 3 && buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF;
|
|
3816
|
-
// 비-ASCII (≥ 0x80) 바이트 존재 여부 검사
|
|
3817
|
-
let nonAscii = false;
|
|
3818
|
-
for (let i = 0; i < Math.min(buf.length, 4096); i++) {
|
|
3819
|
-
if (buf[i] >= 0x80) { nonAscii = true; break; }
|
|
3820
|
-
}
|
|
3821
|
-
if (nonAscii && !hasBOM) {
|
|
3822
|
-
// 1.9.243: CJK 분류 추가
|
|
3823
|
-
const cjk = _classifyCJK(buf, 4096);
|
|
3824
|
-
const label = _riskLabel(cjk);
|
|
3825
|
-
result.atRisk.push({
|
|
3826
|
-
file: path.relative(root, fp),
|
|
3827
|
-
ext: path.extname(fp),
|
|
3828
|
-
hasBOM: false,
|
|
3829
|
-
nonAscii: true,
|
|
3830
|
-
riskType: label.type,
|
|
3831
|
-
cjk,
|
|
3832
|
-
risk: label.risk
|
|
3833
|
-
});
|
|
3834
|
-
}
|
|
3835
|
-
} catch {}
|
|
3836
|
-
}
|
|
3837
|
-
}
|
|
3838
|
-
}
|
|
3839
|
-
walk(root);
|
|
3840
|
-
if (result.atRisk.length > 0) {
|
|
3841
|
-
result.notes.push('해결: 파일 시작에 UTF-8 BOM 추가 (EF BB BF) 또는 .ps1 첫 줄에 $OutputEncoding/[Console]::OutputEncoding 설정');
|
|
3842
|
-
}
|
|
3843
|
-
// 1.9.243: 분류별 통계 요약
|
|
3844
|
-
const types = result.atRisk.reduce((m, r) => { m[r.riskType] = (m[r.riskType] || 0) + 1; return m; }, {});
|
|
3845
|
-
result.riskTypeCounts = types;
|
|
3846
|
-
return result;
|
|
3802
|
+
return scanShellScriptsEncoding(root, { classifyCJK: _classifyCJK, riskLabel: _riskLabel });
|
|
3847
3803
|
}
|
|
3848
3804
|
|
|
3849
3805
|
function envCmd(root, sub) {
|
|
@@ -3863,40 +3819,21 @@ function envCmd(root, sub) {
|
|
|
3863
3819
|
for (const r of result.atRisk) {
|
|
3864
3820
|
try {
|
|
3865
3821
|
const fullPath = path.join(root, r.file);
|
|
3866
|
-
|
|
3867
|
-
//
|
|
3868
|
-
|
|
3869
|
-
const isShebang = orig.length >= 2 && orig[0] === 0x23 && orig[1] === 0x21; // '#!'
|
|
3870
|
-
if (/\.sh$/i.test(r.file) || isShebang) {
|
|
3871
|
-
result.applied.push({ file: r.file, action: 'skipped-shebang (BOM은 shebang을 깨뜨림 — .sh는 no-BOM UTF-8 유지)' });
|
|
3872
|
-
continue;
|
|
3873
|
-
}
|
|
3874
|
-
// 1.35.15 (encoding 헌트, codex #2): .bat/.cmd 는 BOM 추가 금지 — cmd.exe 는 BOM 으로 코드페이지를 바꾸지 않음(첫 줄 'chcp 65001' 이 올바른 해법). BOM 은 무익하고 일부 Windows 에선 첫 명령을 깨뜨림 → 거짓 수정 대신 chcp 안내.
|
|
3875
|
-
if (/\.(bat|cmd)$/i.test(r.file)) {
|
|
3876
|
-
result.applied.push({ file: r.file, action: 'skipped-batch (cmd.exe: 첫 줄에 chcp 65001 추가가 정답 — BOM은 코드페이지를 안 바꿈)' });
|
|
3877
|
-
continue;
|
|
3878
|
-
}
|
|
3879
|
-
// 1.35.15 (encoding 헌트, codex #4 DESTRUCTIVE): 본문이 유효 UTF-8 일 때만 BOM 추가. CP949/Latin-1 등 비-UTF-8 본문에 BOM 만 붙이면 'UTF-8 이라 주장하는 CP949' 손상본이 됨(transcode 없이 파괴적) → skip + 수동 transcode 안내. (mojibake 를 더 악화시키던 자기-파괴 차단, .sh 가드와 동류.)
|
|
3880
|
-
const isValidUtf8 = Buffer.from(orig.toString('utf8'), 'utf8').equals(orig);
|
|
3881
|
-
if (!isValidUtf8) {
|
|
3882
|
-
result.applied.push({ file: r.file, action: 'skipped-nonutf8 (본문이 비-UTF-8(CP949 등) — BOM 추가는 손상. UTF-8 로 먼저 transcode 필요)' });
|
|
3883
|
-
continue;
|
|
3884
|
-
}
|
|
3885
|
-
const bom = Buffer.from([0xEF, 0xBB, 0xBF]);
|
|
3886
|
-
const fixed = Buffer.concat([bom, orig]);
|
|
3887
|
-
fs.writeFileSync(fullPath, fixed);
|
|
3888
|
-
result.applied.push({ file: r.file, action: 'utf8-bom-added' });
|
|
3822
|
+
// 1.36.177 (T-0022): drift/session-close와 같은 최종 사전조건 및
|
|
3823
|
+
// 원자적 binary write를 공유한다. 스캔 뒤 파일이 바뀌어도 쓰기 직전 재검사한다.
|
|
3824
|
+
result.applied.push(applyShellScriptUtf8Bom(fullPath, r.file));
|
|
3889
3825
|
} catch (e) {
|
|
3890
|
-
result.applied.push({ file: r.file, action: 'failed', error: e.message });
|
|
3826
|
+
result.applied.push({ file: r.file, action: 'failed', code: e.code || 'error', error: e.message, ...(e.backupFile ? { backupFile: e.backupFile } : {}), ...(e.recoveryArtifacts ? { recoveryArtifacts: e.recoveryArtifacts } : {}) });
|
|
3891
3827
|
}
|
|
3892
3828
|
}
|
|
3893
3829
|
}
|
|
3894
3830
|
// 1.36.20 (codex fresh-QA #7): apply 실패(read-only EPERM 등)를 exit code 에 반영 — 종전엔 action:'failed' 를 기록만 하고 exit 0.
|
|
3895
|
-
if (apply && (result.applied || []).some(a => a.action === 'failed')) process.exitCode = 1;
|
|
3831
|
+
if (result.scanErrors.length > 0 || (apply && (result.applied || []).some(a => a.action === 'failed'))) process.exitCode = 1;
|
|
3896
3832
|
if (has('--json')) { log(JSON.stringify(result, null, 2)); return; }
|
|
3897
3833
|
log(cy(`# leerness env encoding-check (1.9.241/242, UR-0014) — 셸 스크립트 인코딩 위험 감지`));
|
|
3898
3834
|
log('');
|
|
3899
3835
|
log(` 스캔: ${gr(String(result.scanned))} 파일 (.ps1/.bat/.cmd/.sh)`);
|
|
3836
|
+
if (result.scanErrors.length > 0) log(rd(` ⚠ 읽기 실패 ${result.scanErrors.length}건 — 검사가 불완전함`));
|
|
3900
3837
|
if (result.atRisk.length === 0) {
|
|
3901
3838
|
log(gr(` ✓ 인코딩 위험 없음`));
|
|
3902
3839
|
} else {
|
|
@@ -3908,8 +3845,10 @@ function envCmd(root, sub) {
|
|
|
3908
3845
|
log('');
|
|
3909
3846
|
if (apply) {
|
|
3910
3847
|
const okCnt = (result.applied || []).filter(a => a.action === 'utf8-bom-added').length;
|
|
3848
|
+
const skipCnt = (result.applied || []).filter(a => a.action.startsWith('skipped-')).length;
|
|
3911
3849
|
const failCnt = (result.applied || []).filter(a => a.action === 'failed').length;
|
|
3912
|
-
log(gr(` ✓ UTF-8 BOM 추가 ${okCnt}건${failCnt > 0 ? `
|
|
3850
|
+
log(gr(` ✓ UTF-8 BOM 추가 ${okCnt}건${skipCnt > 0 ? ` · 안전상 건너뜀 ${skipCnt}건` : ''}${failCnt > 0 ? ` · 실패 ${failCnt}건` : ''} (1.9.242 --apply)`));
|
|
3851
|
+
for (const a of (result.applied || []).filter(a => a.backupFile)) log(yl(` ↳ 원본/동시 편집 복구본 보존: ${a.backupFile}`));
|
|
3913
3852
|
} else {
|
|
3914
3853
|
result.notes.forEach(n => log(yl(` 💡 ${n}`)));
|
|
3915
3854
|
log(dm(` → 자동 수정: leerness env encoding --apply (BOM 자동 추가)`));
|
|
@@ -4345,7 +4284,7 @@ function _selfTestCases() {
|
|
|
4345
4284
|
const s = read(__filename);
|
|
4346
4285
|
const reuseOk = s.includes('project: path.basename(root), found, applied, added') && s.includes("failJson(has('--json'), 'no_reuse_map'"); // reuse: apply 먼저 → json 에 applied/added
|
|
4347
4286
|
const releaseOk = s.includes('deleteCount: toDelete.length, deleted, deleteFailed') && s.includes('if (deleteFailed > 0) process.exitCode = 1'); // release: 삭제 먼저 → json 에 deleted
|
|
4348
|
-
const encOk = s.includes("(result.applied || []).some(a => a.action === 'failed'))
|
|
4287
|
+
const encOk = s.includes("result.scanErrors.length > 0 || (apply && (result.applied || []).some(a => a.action === 'failed'))"); // encoding: 스캔/적용 실패 exit1
|
|
4349
4288
|
return reuseOk && releaseOk && encOk;
|
|
4350
4289
|
} },
|
|
4351
4290
|
{ name: '미검토표면 헌트 (1.36.28, codex): #8 impact 역의존 전이 + #10 alias 단어경계 + #3 손상스토어 클로버 차단 + #4 URL 해시 — 순수/소스 행위검사', run: () => {
|
|
@@ -5188,7 +5127,31 @@ function _selfTestCases() {
|
|
|
5188
5127
|
{ name: '8번째 버그헌트 (UR-0110): rule/decision/lesson add 동시쓰기 _withLock 직렬화 (UR-0043 갭 메움) (1.9.406)', run: () => { const src = read(__filename); const L = '_withLock('; const ruleLock = src.includes(L + 'rulesPath' + '(root), () =>'); const decLock = src.includes(L + 'decisionsJsonPath' + '(root), () =>'); const lesLock = src.includes(L + 'lessonsJsonPath' + '(root), () =>'); return ruleLock && decLock && lesLock; } },
|
|
5189
5128
|
{ name: '8번째 버그헌트 (UR-0111): MCP feature_link safe-write tier(권한경계) + _parseLimit NaN/음수 가드 (1.9.407)', run: () => { const m = require('../lib/pure-utils'); const pl = m._parseLimit('abc', 10) === 10 && m._parseLimit('-5', 10) === 10 && m._parseLimit('0', 10) === 10 && m._parseLimit('3', 10) === 3 && m._parseLimit('7.9', 10) === 7; const tools = require('../lib/mcp-tools'); const arr = Array.isArray(tools) ? tools : (tools.MCP_TOOLS || []); const fl = arr.find(x => x.name === 'leerness_feature_link'); const tierOk = !!fl && fl.requiredTier === 'safe-write'; return pl && tierOk; } },
|
|
5190
5129
|
{ name: '8번째 버그헌트 (UR-0112): _parseSkillMd CRLF/CR 줄바꿈 정규화 (Windows SKILL.md meta 소실 차단) (1.9.408)', run: () => { const m = require('../lib/pure-utils'); const lf = m._parseSkillMd('---\nname: s\ndescription: d\n---\nbody'); const crlf = m._parseSkillMd('---\r\nname: s\r\ndescription: d\r\n---\r\nbody'); const cr = m._parseSkillMd('---\rname: s\rdescription: d\r---\rbody'); const bom = m._parseSkillMd('---\r\nname: s\r\n---\r\nbody'); return lf.meta.name === 's' && crlf.meta.name === 's' && crlf.meta.description === 'd' && cr.meta.name === 's' && bom.meta.name === 's'; } },
|
|
5191
|
-
{ name: '8번째 버그헌트 (UR-0113): env encoding-check --apply
|
|
5130
|
+
{ name: '8번째 버그헌트 (UR-0113): env encoding-check --apply 셸/플랫폼/모호 인코딩 안전계약 (1.9.409/T-0022)', run: () => {
|
|
5131
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_bom_'));
|
|
5132
|
+
try {
|
|
5133
|
+
const sh = path.join(tmp, 's.sh');
|
|
5134
|
+
const ps = path.join(tmp, 's.ps1');
|
|
5135
|
+
const ambiguous = path.join(tmp, 'ambiguous.ps1');
|
|
5136
|
+
fs.writeFileSync(sh, '#!/bin/bash\n# 한글\necho hi\n');
|
|
5137
|
+
fs.writeFileSync(ps, 'Write-Host "😀"\n');
|
|
5138
|
+
fs.writeFileSync(ambiguous, Buffer.from([0x41, 0xC2, 0xA1, 0x42]));
|
|
5139
|
+
const save = process.argv; let out = ''; const _w = process.stdout.write;
|
|
5140
|
+
try {
|
|
5141
|
+
process.argv = ['node', 'h', 'env', 'encoding-check', '--path', tmp, '--apply', '--json'];
|
|
5142
|
+
process.stdout.write = s => { out += s; return true; };
|
|
5143
|
+
envCmd(tmp, 'encoding-check');
|
|
5144
|
+
} catch {} finally { process.stdout.write = _w; process.argv = save; }
|
|
5145
|
+
const shBuf = fs.readFileSync(sh);
|
|
5146
|
+
const psBuf = fs.readFileSync(ps);
|
|
5147
|
+
const ambiguousBuf = fs.readFileSync(ambiguous);
|
|
5148
|
+
const shNoBom = !shBuf.subarray(0, 3).equals(Buffer.from([0xEF, 0xBB, 0xBF])) && shBuf[0] === 0x23 && shBuf[1] === 0x21;
|
|
5149
|
+
const platformOk = process.platform === 'win32'
|
|
5150
|
+
? psBuf.subarray(0, 3).equals(Buffer.from([0xEF, 0xBB, 0xBF]))
|
|
5151
|
+
: psBuf.equals(Buffer.from('Write-Host "😀"\n'));
|
|
5152
|
+
return shNoBom && platformOk && ambiguousBuf.equals(Buffer.from([0x41, 0xC2, 0xA1, 0x42]));
|
|
5153
|
+
} finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
5154
|
+
} },
|
|
5192
5155
|
{ name: '8번째 버그헌트 (UR-0114): absRoot 비문자열(--path 값없음 boolean true) → cwd 폴백(raw TypeError 차단) (1.9.410)', run: () => { const io = require('../lib/io'); const cwd = process.cwd(); const tBool = io.absRoot(true) === cwd; const tEmpty = io.absRoot('') === cwd; const tUndef = io.absRoot(undefined) === cwd; const tSpace = io.absRoot(' ') === cwd; const tReal = io.absRoot(os.tmpdir()) === path.resolve(os.tmpdir()); return tBool && tEmpty && tUndef && tSpace && tReal; } },
|
|
5193
5156
|
{ name: '8번째 버그헌트 (UR-0115): lazy detect --auto-track 단일 RMW 배치(O(T×N)→O(N+T)) (1.9.411)', run: () => { const src = read(__filename); const batched = src.includes("8번째 버그헌트, UR-0115") && /has\('--auto-track'\)[\s\S]{0,500}?_withLock\(progressPath\(root\), \(\) => \{[\s\S]{0,1200}?writeProgressRows/.test(src); const noPerTodoUpsert = !/for \(const t of newTodos\) \{\s*const id = nextId\(root, 'T'\);/.test(src); return batched && noPerTodoUpsert; } },
|
|
5194
5157
|
{ name: '6번째 외부평가 Opus P1 (UR-0100): list-family(decision/feature/plan/runs/team list) positional path 지원 (조용한 cwd 오독 차단) (1.9.412)', run: () => { const src = read(__filename); const L = '_resolveRoot('; const decOk = src.includes("decisionListCmd(absRoot(" + L + "args[2]))"); const planOk = src.includes("planListCmd(absRoot(" + L + "args[2]))"); const featOk = src.includes("featureListCmd(absRoot(" + L + "args[2]))"); const runsOk = src.includes("runsListCmd(absRoot(" + L + "args[2]))"); const teamOk = src.includes(L + "args[1] === 'list' ? args[2] : null)"); return decOk && planOk && featOk && runsOk && teamOk; } },
|
|
@@ -5381,7 +5344,7 @@ function _selfTestCases() {
|
|
|
5381
5344
|
const delegated = src.includes("require('../lib/drift')") && src.includes('_drift.driftCheckCmd(root, opts,');
|
|
5382
5345
|
const modSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'drift.js'));
|
|
5383
5346
|
const bodyMarker = 'auto-fix 활성 ' + '(1.9.82)'; // drift 본문 고유(split-literal 자기참조 회피)
|
|
5384
|
-
const recursionWired =
|
|
5347
|
+
const recursionWired = /driftCheckCmd\(root,\s*\{\s*\.\.\.opts,\s*_noAutoFix:\s*true,\s*_encodingFixErrors:\s*encodingFixErrors,\s*_encodingAutoFix:\s*encodingAutoFix,\s*_autoFixSummary:\s*autoFixSummary\s*\},\s*deps\)/.test(modSrc); // 재귀에 deps + 전체 단계 실패/요약 누적 전달 + depth 가드
|
|
5385
5348
|
const movedToLib = modSrc.includes("require('./io')") && recursionWired && modSrc.includes(bodyMarker) && !src.includes(bodyMarker);
|
|
5386
5349
|
let behavOk = false;
|
|
5387
5350
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_drift_'));
|
|
@@ -5498,7 +5461,7 @@ function _selfTestCases() {
|
|
|
5498
5461
|
{ name: '10th 외부평가 UR-0131 잔여: drift --auto-fix 재귀 depth 가드(_noAutoFix) — 무한재귀 방지 (1.9.432)', run: () => {
|
|
5499
5462
|
const modSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'drift.js'));
|
|
5500
5463
|
const guard = modSrc.includes("has('--auto-fix') && !opts._noAutoFix");
|
|
5501
|
-
const recurseGuarded = (modSrc.match(/driftCheckCmd\(root
|
|
5464
|
+
const recurseGuarded = (modSrc.match(/driftCheckCmd\(root,\s*\{\s*\.\.\.opts,\s*_noAutoFix:\s*true,\s*_encodingFixErrors:\s*encodingFixErrors,\s*_encodingAutoFix:\s*encodingAutoFix,\s*_autoFixSummary:\s*autoFixSummary\s*\},\s*deps\)/g) || []).length === 1;
|
|
5502
5465
|
return guard && recurseGuarded;
|
|
5503
5466
|
} },
|
|
5504
5467
|
{ name: '11th 외부평가 Codex/Opus P1: contract spec backtick-bullet 함수/필드 강선언 인식 (1.9.433)', run: () => {
|
|
@@ -16876,6 +16839,8 @@ function handoff(root) {
|
|
|
16876
16839
|
shellScriptsScanned: encScan.scanned,
|
|
16877
16840
|
encodingRiskCount: encScan.atRisk.length,
|
|
16878
16841
|
encodingRiskFiles: encScan.atRisk.slice(0, 5).map(r => r.file),
|
|
16842
|
+
encodingScanErrorCount: (encScan.scanErrors || []).length,
|
|
16843
|
+
encodingScanErrors: (encScan.scanErrors || []).slice(0, 5),
|
|
16879
16844
|
// 1.9.249 (UR-0018): 터미널 출력 인코딩 안전 여부 + 자동 회복 결과
|
|
16880
16845
|
terminalEncodingOk: runtimeEnv.locale.codepage === 65001 || !runtimeEnv.locale.isKoreanWindows,
|
|
16881
16846
|
autoChcpApplied: process.env._LEERNESS_AUTOCHCP_APPLIED || null,
|
|
@@ -17434,6 +17399,11 @@ function handoff(root) {
|
|
|
17434
17399
|
// 한국어 Windows + .ps1/.bat 비-ASCII + BOM 없음 → CP949 오인식 위험
|
|
17435
17400
|
try {
|
|
17436
17401
|
const scan = _scanShellScriptsEncoding(root);
|
|
17402
|
+
if (scan.scanErrors && scan.scanErrors.length > 0) {
|
|
17403
|
+
log('');
|
|
17404
|
+
log(`## ⚠ 셸 스크립트 인코딩 스캔 읽기 실패 ${scan.scanErrors.length}건 — 결과 불완전`);
|
|
17405
|
+
scan.scanErrors.slice(0, 3).forEach(e => log(` • ${e.file}: ${e.code}`));
|
|
17406
|
+
}
|
|
17437
17407
|
if (scan.atRisk && scan.atRisk.length > 0) {
|
|
17438
17408
|
const isTty = process.stdout && process.stdout.isTTY;
|
|
17439
17409
|
const rd5 = s => isTty ? `\x1b[31m${s}\x1b[0m` : s;
|
package/lib/agents.js
CHANGED
|
@@ -401,7 +401,11 @@ function agentsCmd(root, sub, args = [], deps = {}) {
|
|
|
401
401
|
if (!task) { fail('bench "<task>" 필요'); return process.exit(1); }
|
|
402
402
|
const timeoutS = parseInt(arg('--timeout', '60'), 10);
|
|
403
403
|
const writeMode = has('--write');
|
|
404
|
-
|
|
404
|
+
// Disabled providers are not bench candidates. Filter before _checkAgent so a
|
|
405
|
+
// bench that has no opted-in provider cannot execute installed third-party
|
|
406
|
+
// CLIs merely to discover that they are disabled.
|
|
407
|
+
const ready = EXTERNAL_AGENTS.filter(a => process.env[a.envFlag] === '1')
|
|
408
|
+
.map(a => ({ agent: a, status: _checkAgent(a) }))
|
|
405
409
|
.filter(x => x.status.status === 'ready');
|
|
406
410
|
if (!ready.length) {
|
|
407
411
|
fail('ready CLI 없음 — leerness setup-agents 또는 .env에 LEERNESS_ENABLE_X=1 설정 필요');
|