leerness 1.36.176 → 1.36.178
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/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 -8
- package/scripts/mutation-integrity-probe.js +536 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.178 — 2026-08-29
|
|
4
|
+
|
|
5
|
+
- CI: env encoding-check --apply 전체 E2E를 Windows BOM 교체와 POSIX byte-exact no-op 계약으로 분기해 v1.36.177 Linux/Node 22의 466/467 실패를 수정.
|
|
6
|
+
|
|
7
|
+
## 1.36.177 — 2026-08-29
|
|
8
|
+
|
|
9
|
+
- T-0022 데이터 무결성: env/drift/session BOM 수정을 공용 CAS·Windows ReplaceFile 경계로 통합하고 CP949·모호 인코딩·손상 BOM·메타데이터·동시 writer/ADS·부분진행 복구를 실패-폐쇄 처리. auto-fix 전 단계 JSON/exit 정직화, 스캔 I/O 오류 및 unborn/실행불가 Git 구분, OS 매트릭스 mutation-integrity 회귀 추가.
|
|
10
|
+
|
|
3
11
|
## 1.36.176 — 2026-08-29
|
|
4
12
|
|
|
5
13
|
- T-0089: agents bench가 활성화되지 않은 provider의 버전 명령까지 실행하던 환경 의존성을 제거했습니다. bench 후보를 opt-in provider로 먼저 제한하고, E2E는 레지스트리의 10개 플래그를 모두 비활성화한 뒤 PATH 선두 실행 마커가 생성되지 않는지 검증합니다. 수정 후 동일 Windows Node 26 환경 직접 프로브 3회는 394/362/342ms(최대 394ms)였으며, 기존 15초 제한은 최댓값 대비 약 38.1배 여유라 불필요한 상향을 하지 않습니다.
|
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.178 하네스를 사용합니다. 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.178는 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.176는 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.178 릴리스 태그 이력** (수백 라운드) · _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.178: 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.178';
|
|
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;
|