leerness 1.36.63 → 1.36.65
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 +73 -12
- package/lib/io.js +3 -0
- package/lib/pure-utils.js +9 -1
- package/package.json +5 -4
- package/scripts/e2e.js +37 -1
- package/scripts/lint.js +63 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.65 — 2026-07-22 — no-op 재설치 + 동일-내용 쓰기 스킵 (외부감사 F-08) — R-0001 검수 10회전
|
|
4
|
+
|
|
5
|
+
무변경 재init 의 백업/아카이브/타임스탬프 증식(감사 지적) 해소 — 거짓 no-op 은 검수가 차단.
|
|
6
|
+
|
|
7
|
+
- **no-op 재설치**: coreFiles 계산 결과가 현재와 전부 동일하면(휘발 시각키·아카이브 경로 정규화 비교) 백업 생성·파일쓰기·마이그레이션 리포트 생략. force/dry/언어 전환/버전 차이/커스텀 변경은 정상 설치. `LEERNESS_DEBUG_NOOP=1` 진단.
|
|
8
|
+
- **(검수 HIGH) no-op ≠ 조기 종료**: 하위 조정기(.env/.gitignore/훅/스킬 payload/roadmap)는 항상 계속 — 조기 return 은 `--no-env→기본 재init` 등 6케이스에서 요청된 복구를 건너뛰었다(검수 실측). 무변경 churn 은 **writeUtf8 동일-내용 스킵**이 전역 해소.
|
|
9
|
+
- **(파생 선재버그) _mergeLines substring 흡수**: '.env' 가 '.env.local' 에 포함돼 gitignore 복구가 영영 안 되던 것 — 라인 단위 정확 매칭으로 수정.
|
|
10
|
+
- (검수 Low) 래퍼 정규화는 백틱 경로만(문구 변경은 실변경 감지). update.md 이중 출처 단일화(coreFiles↔훅 설치기).
|
|
11
|
+
- 실측: 기본/minimal/en/skills 재init no-op·archive/manifest 불변·.env/gitignore 복구 계속·커스텀 1회 유지·구버전/전환 정상 설치. e2e +1, selftest 333.
|
|
12
|
+
|
|
13
|
+
## 1.36.64 — 2026-07-22 — lint 게이트 (외부감사 F-10) — R-0001 검수 9회전
|
|
14
|
+
|
|
15
|
+
과거 실사고(raw U+2028 정규식 리터럴 → 전 명령 SyntaxError 출하) 재발을 구조적으로 차단하는 0-deps lint.
|
|
16
|
+
|
|
17
|
+
- **`scripts/lint.js` + `npm run lint`**: 배포 js 전수 node --check 구문 · U+2028/U+2029 감지 · BOM · JSON 유효성 (~3초). `npm test`/`test:fast`/(검수 #1)`test:core` 전 티어 선두 배선.
|
|
18
|
+
- 구현 중 자기참조 트랩 재발(6번째)을 자체 검출로 확인 — 검사 문자를 \u 이스케이프 표기로.
|
|
19
|
+
- (검수 #2) selftest 를 4축 전부 행위 검증으로(구문/U+2028/BOM/JSON 각각 검출 + 클린 통과 + 전 티어 배선). (검수 #3) 스폰 실패를 구문오류와 구분 + SyntaxError 본문 라인 우선 표시.
|
|
20
|
+
- 검수 통과 프로브: 실 28파일 클린 · FP 0(\u 이스케이프 텍스트/250KB 라인/CRLF) · 패키지 동봉·설치본 루트 해석 정상.
|
|
21
|
+
|
|
3
22
|
## 1.36.63 — 2026-07-22 — F-05 시리즈 완결: README 섹션·헤더·런타임 기본값 en (5회차) — R-0001 검수 8회전
|
|
4
23
|
|
|
5
24
|
**en init 한글 잔존 53/61 → 6/61** — 잔여 6개 전부 스킬팩 한국어 콘텐츠(데이터, 의도적 제외). 감사 F-05 종결.
|
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.65 하네스를 사용합니다. 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.65는 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.63는 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.65 릴리스 태그 이력** (수백 라운드) · _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.65: 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.65';
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -745,7 +745,8 @@ leerness memory restore <surface> <target> # archive → active 복귀 (DELETE
|
|
|
745
745
|
'.claude/commands/session-close.md': `# /session-close\n\n세션 종료 보고를 자동 생성하고 session-handoff.md를 갱신합니다.\n\n\`\`\`\n!leerness session close .\n\`\`\`\n`,
|
|
746
746
|
'.claude/commands/audit.md': `# /audit\n\n계획-진행 정렬, 디자인/재사용 일관성, 시크릿/인코딩을 일괄 점검합니다.\n\n\`\`\`\n!leerness audit .\n!leerness scan secrets .\n!leerness encoding check .\n\`\`\`\n`,
|
|
747
747
|
'.claude/commands/lazy-detect.md': `# /lazy-detect\n\n게으름 방지 자동 평가를 실행합니다.\n\n\`\`\`\n!leerness lazy detect .\n\`\`\`\n`,
|
|
748
|
-
|
|
748
|
+
// 1.36.65: auto-update 훅 설치기와 동일 내용(단일화) — 재init no-op 판정이 어긋나지 않게
|
|
749
|
+
'.claude/commands/update.md': `# /update\n\nleerness 자동 업데이트 (감지 → 마이그레이션 → 검증).\n\n\`\`\`\n!leerness update --yes\n\`\`\`\n\n체크만:\n\n\`\`\`\n!leerness update --check\n\`\`\`\n`,
|
|
749
750
|
'.claude/skills/leerness.md': `---\nname: leerness\ndescription: Leerness harness commands - handoff, audit, scan secrets, encoding check, lazy detect, session close, update. Use when the user asks to load project context, verify work quality, scan secrets, check encoding, or end a session.\n---\n\n# leerness skill\n\n## When to use\n- 사용자가 프로젝트 컨텍스트를 로드해달라고 할 때\n- 완료 선언 전 자기 검증을 요청할 때\n- 세션을 종료하거나 인수인계를 요청할 때\n- 시크릿/한글 인코딩 점검을 요청할 때\n- 새 leerness 버전 적용을 요청할 때\n\n## Commands\n\n\`\`\`bash\nleerness handoff . # 컨텍스트 로드\nleerness check . # pre-action 체크\nleerness audit . # 일관성/계획 정렬 감사\nleerness scan secrets . # 시크릿 패턴 스캔\nleerness encoding check . # UTF-8/BOM/NUL\nleerness lazy detect . # 게으름 평가\nleerness memory search "key" # 결정/이력 검색\nleerness session close . # 종료 보고 + handoff 자동 생성\nleerness update --yes # 자동 업데이트\n\`\`\`\n`,
|
|
750
751
|
};
|
|
751
752
|
// 1.36.59 (외부감사 F-05 시리즈 1회차): en 프로젝트에서 에이전트가 "지시"로 읽는 최상위 5종은 완전 영어 —
|
|
@@ -767,7 +768,7 @@ leerness memory restore <surface> <target> # archive → active 복귀 (DELETE
|
|
|
767
768
|
_files['.claude/commands/session-close.md'] = `# /session-close\n\nGenerate the end-of-session report and update session-handoff.md.\n\n\`\`\`\n!leerness session close .\n\`\`\`\n`;
|
|
768
769
|
_files['.claude/commands/audit.md'] = `# /audit\n\nCheck plan-progress alignment, design/reuse consistency, secrets, and encoding in one pass.\n\n\`\`\`\n!leerness audit .\n!leerness scan secrets .\n!leerness encoding check .\n\`\`\`\n`;
|
|
769
770
|
_files['.claude/commands/lazy-detect.md'] = `# /lazy-detect\n\nRun the lazy-work (false-done) automatic evaluation.\n\n\`\`\`\n!leerness lazy detect .\n\`\`\`\n`;
|
|
770
|
-
_files['.claude/commands/update.md'] = `# /update\n\nRun the leerness auto-update (detect → migrate → verify).\n\n\`\`\`\n!leerness update --yes\n\`\`\`\n`;
|
|
771
|
+
_files['.claude/commands/update.md'] = `# /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`;
|
|
771
772
|
_files['.claude/skills/leerness.md'] = `---\nname: leerness\ndescription: Leerness harness commands - handoff, audit, scan secrets, encoding check, lazy detect, session close, update. Use when the user asks to load project context, verify work quality, scan secrets, check encoding, or end a session.\n---\n\n# leerness skill\n\n## When to use\n- When the user asks to load project context\n- When self-verification is requested before claiming completion\n- When ending a session or asked for a handoff\n- When secret/encoding checks are requested\n- When applying a new leerness version\n\n## Commands\n\n\`\`\`bash\nleerness handoff . # load context\nleerness check . # pre-action check\nleerness audit . # consistency/plan-alignment audit\nleerness scan secrets . # secret pattern scan\nleerness encoding check . # UTF-8/BOM/NUL\nleerness lazy detect . # lazy-work evaluation\nleerness memory search "key" # search decisions/history\nleerness session close . # close report + auto handoff\nleerness update --yes # auto-update\n\`\`\`\n`;
|
|
772
773
|
// 1.36.61 (F-05 3회차): .harness 정책·시드 문서군 en 완역 — 지시 5종(1.36.59)에 이어 read-order 가 가리키는 참조층.
|
|
773
774
|
_files['.harness/project-brief.md'] = fm('project-brief', ['confirming project purpose', 'judging new features', 'planning'], ['project purpose changes', 'users/scope change'], `# Project Brief\n\n## Project\n${project}\n\n## Purpose\n- Update this with the real purpose of the project.\n\n## Users\n-\n\n## Success Criteria\n-\n`, 'en');
|
|
@@ -1130,14 +1131,6 @@ async function install(root, opts = {}) {
|
|
|
1130
1131
|
const _altLang = lang === 'en' ? 'ko' : 'en';
|
|
1131
1132
|
let _altFiles = {};
|
|
1132
1133
|
if (_langSwitched) { try { _altFiles = coreFiles(root, _altLang, skills, { minimal: !!opts.minimal }); } catch {} }
|
|
1133
|
-
const backup = createBackup(root, opts.force ? 'force' : (opts.migration ? 'migration' : 'init'), files, opts.dry);
|
|
1134
|
-
if (opts.dry) {
|
|
1135
|
-
log(`Backup target: ${backup.archiveDir}`);
|
|
1136
|
-
log('Files that would be backed up:');
|
|
1137
|
-
backup.candidates.forEach(f => log('- ' + f));
|
|
1138
|
-
} else {
|
|
1139
|
-
ok(`backup created: ${rel(root, backup.archiveDir)}`);
|
|
1140
|
-
}
|
|
1141
1134
|
const managedOverwrite = new Set([
|
|
1142
1135
|
'AGENTS.md','CLAUDE.md','.cursor/rules/leerness.mdc','.github/copilot-instructions.md',
|
|
1143
1136
|
'.harness/session-workflow.md', // 1.36.60 (검수 #1): managed 편입 — 종전엔 재init 시 구 언어/구 버전 그대로 방치
|
|
@@ -1155,6 +1148,45 @@ async function install(root, opts = {}) {
|
|
|
1155
1148
|
'.claude/commands/handoff.md','.claude/commands/session-close.md','.claude/commands/audit.md','.claude/commands/lazy-detect.md','.claude/commands/update.md',
|
|
1156
1149
|
'.claude/skills/leerness.md'
|
|
1157
1150
|
]);
|
|
1151
|
+
// 1.36.65 (외부감사 F-08): no-op 재설치 감지 — 계산 결과가 현재와 전부 동일하면 백업/쓰기/타임스탬프를 만들지 않는다.
|
|
1152
|
+
// 비교는 휘발 필드 정규화 후: manifest/lock 의 시각류 키, preserved 래퍼의 아카이브 경로 라인.
|
|
1153
|
+
// force/dry/마이그레이션(버전 변화)·언어 전환은 no-op 판정 대상 아님.
|
|
1154
|
+
const _normVolatile = (f, s) => {
|
|
1155
|
+
let t = String(s);
|
|
1156
|
+
if (/\.json$/i.test(f)) t = t.replace(/"(installedAt|generatedAt|updatedAt|at)"\s*:\s*"[^"]*"/g, '"$1":"~"');
|
|
1157
|
+
t = t.replace(/(backup|백업): ?`[^`]*`/gi, '$1: `~`'); // (검수 2차 Low) 경로만 정규화 — 래퍼 문구 변경은 실변경으로 감지
|
|
1158
|
+
return t.replace(/\r\n/g, '\n').trim();
|
|
1159
|
+
};
|
|
1160
|
+
let _noop = !opts.force && !opts.dry && !_langSwitched && exists(path.join(root, '.harness'));
|
|
1161
|
+
if (_noop) {
|
|
1162
|
+
for (const [f, c] of Object.entries(files)) {
|
|
1163
|
+
const p = path.join(root, f);
|
|
1164
|
+
if (!exists(p)) { _noop = false; if (process.env.LEERNESS_DEBUG_NOOP) log(`[noop-debug] missing: ${f}`); break; }
|
|
1165
|
+
if (managedOverwrite.has(f)) {
|
|
1166
|
+
let cur = ''; try { cur = read(p); } catch { _noop = false; break; }
|
|
1167
|
+
const wouldBe = managedMerge(f, c, cur, null, { altTemplate: _altFiles[f], lang });
|
|
1168
|
+
if (_normVolatile(f, wouldBe) !== _normVolatile(f, cur)) { _noop = false; if (process.env.LEERNESS_DEBUG_NOOP) log(`[noop-debug] differs: ${f}`); break; }
|
|
1169
|
+
}
|
|
1170
|
+
// 비-managed 는 preserve(불변) 경로 — 존재하면 변경 없음
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
// (검수 2차 HIGH): no-op 이어도 하위 조정기(.env/.gitignore/훅/스킬 payload/roadmap)는 계속 실행 —
|
|
1174
|
+
// 조기 return 은 "요청된 드리프트 복구"까지 건너뛰었다(--no-env→기본 재init 시 .env 미생성 등 6케이스 실측).
|
|
1175
|
+
// 생략 대상은 백업 생성 + coreFiles 쓰기 루프 + 마이그레이션 리포트뿐. 조정기의 무변경 churn 은
|
|
1176
|
+
// writeUtf8 동일-내용 스킵(1.36.65)이 전역 해소.
|
|
1177
|
+
let backup = { archiveDir: path.join(root, '.harness', 'archive'), candidates: [], skipped: true };
|
|
1178
|
+
if (_noop) {
|
|
1179
|
+
ok(`no-op: coreFiles 이미 v${VERSION} 최신 — 백업/파일쓰기 생략, 부속 구성(.env/훅/스킬)은 정합 확인 계속 (강제: --force)`);
|
|
1180
|
+
} else {
|
|
1181
|
+
backup = createBackup(root, opts.force ? 'force' : (opts.migration ? 'migration' : 'init'), files, opts.dry);
|
|
1182
|
+
if (opts.dry) {
|
|
1183
|
+
log(`Backup target: ${backup.archiveDir}`);
|
|
1184
|
+
log('Files that would be backed up:');
|
|
1185
|
+
backup.candidates.forEach(f => log('- ' + f));
|
|
1186
|
+
} else {
|
|
1187
|
+
ok(`backup created: ${rel(root, backup.archiveDir)}`);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1158
1190
|
// 1.9.184 (사용자 명시): 파일 설치 — 생성 목록 나열 X, 로딩바 + 단일 완료 메시지.
|
|
1159
1191
|
const actions = [];
|
|
1160
1192
|
const totalFiles = Object.keys(files).length;
|
|
@@ -1173,6 +1205,7 @@ async function install(root, opts = {}) {
|
|
|
1173
1205
|
// 덮어써 활성 기록이 archive 에만 남았다. force 는 가이드/템플릿 문서에만 적용 — 상태 파일은 force 여도 보존.
|
|
1174
1206
|
const _USER_STATE = new Set(['.harness/progress-tracker.md', '.harness/plan.md', '.harness/task-log.md', '.harness/decisions.md', '.harness/current-state.md', '.harness/session-handoff.md', '.harness/rules.md', '.harness/feature-graph.md', '.harness/reuse-map.md', '.harness/feature-contracts.md', '.harness/project-brief.md', '.harness/architecture.md', '.harness/context-map.md', '.harness/design-system.md', '.harness/review-evidence.md']); // 1.36.62 (검수 #3): 검증 이력도 사용자 데이터
|
|
1175
1207
|
for (const [f, c] of Object.entries(files)) {
|
|
1208
|
+
if (_noop) break; // 1.36.65: coreFiles 무변경 — 쓰기 루프 생략 (하위 조정기는 아래에서 계속)
|
|
1176
1209
|
const existsNow = exists(path.join(root, f));
|
|
1177
1210
|
const mergeManaged = managedOverwrite.has(f);
|
|
1178
1211
|
const effForce = opts.force && !_USER_STATE.has(f);
|
|
@@ -1289,7 +1322,7 @@ async function install(root, opts = {}) {
|
|
|
1289
1322
|
syncReadme(root);
|
|
1290
1323
|
installSkills(root, skills);
|
|
1291
1324
|
// 1.9.41: migrate 시 이전 버전을 미리 캡처해 차분 안내에 사용
|
|
1292
|
-
writeMigrationReport(root, backup, actions, { fromV: opts._previousVersion || null });
|
|
1325
|
+
if (!_noop) writeMigrationReport(root, backup, actions, { fromV: opts._previousVersion || null }); // 1.36.65: no-op 시 리포트 무증식
|
|
1293
1326
|
// 1.9.41: migrate 후 (= 점프인 경우) 차분 안내를 stdout에 즉시 출력 — AI 컨텍스트에 새 도구 주입
|
|
1294
1327
|
if (opts.migration && opts._previousVersion && opts._previousVersion !== VERSION) {
|
|
1295
1328
|
try {
|
|
@@ -5373,6 +5406,34 @@ function _selfTestCases() {
|
|
|
5373
5406
|
const idem = (m3.match(/MY CUSTOM LINE/g) || []).length === 1;
|
|
5374
5407
|
const wired = read(__filename).includes('altTemplate: _altFiles[f]') && read(__filename).includes("'.harness/session-workflow.md', // 1.36.60");
|
|
5375
5408
|
return noKoTemplate && customKept && enWrapper && control && idem && wired;
|
|
5409
|
+
} },
|
|
5410
|
+
{ name: 'lint 게이트 (1.36.64, 외부감사 F-10): 구문/U+2028(실사고 재발 차단)/BOM/JSON — 클린 통과 + 위험문자 검출 행위검사 + npm test 배선', run: () => {
|
|
5411
|
+
const lintP = path.join(__dirname, '..', 'scripts', 'lint.js');
|
|
5412
|
+
if (!exists(lintP)) return false;
|
|
5413
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_lint_'));
|
|
5414
|
+
try {
|
|
5415
|
+
fs.mkdirSync(path.join(tmp, 'lib'), { recursive: true });
|
|
5416
|
+
fs.writeFileSync(path.join(tmp, 'lib', 'ok.js'), 'module.exports = 1;\n');
|
|
5417
|
+
const r1 = cp.spawnSync(process.execPath, [lintP, tmp], { encoding: 'utf8', timeout: 60000 });
|
|
5418
|
+
const cleanOk = r1.status === 0;
|
|
5419
|
+
fs.writeFileSync(path.join(tmp, 'lib', 'bad.js'), 'const s = "a' + String.fromCharCode(0x2028) + 'b";\n');
|
|
5420
|
+
const r2 = cp.spawnSync(process.execPath, [lintP, tmp], { encoding: 'utf8', timeout: 60000 });
|
|
5421
|
+
const caught = r2.status === 1 && /U\+2028/.test(r2.stdout);
|
|
5422
|
+
// (검수 #2) 4개 축 전부 행위 검증 — 구문/BOM/JSON 도 각각 검출
|
|
5423
|
+
fs.writeFileSync(path.join(tmp, 'lib', 'bad.js'), 'const = ;\n');
|
|
5424
|
+
const rSyn = cp.spawnSync(process.execPath, [lintP, tmp], { encoding: 'utf8', timeout: 60000 });
|
|
5425
|
+
const synCaught = rSyn.status === 1 && /syntax:/.test(rSyn.stdout);
|
|
5426
|
+
fs.writeFileSync(path.join(tmp, 'lib', 'bad.js'), Buffer.concat([Buffer.from([0xEF, 0xBB, 0xBF]), Buffer.from('module.exports=1;\n')]));
|
|
5427
|
+
const rBom = cp.spawnSync(process.execPath, [lintP, tmp], { encoding: 'utf8', timeout: 60000 });
|
|
5428
|
+
const bomCaught = rBom.status === 1 && /bom:/.test(rBom.stdout);
|
|
5429
|
+
fs.writeFileSync(path.join(tmp, 'lib', 'bad.js'), 'module.exports=1;\n');
|
|
5430
|
+
fs.writeFileSync(path.join(tmp, 'lib', 'x.json'), '{broken');
|
|
5431
|
+
const rJson = cp.spawnSync(process.execPath, [lintP, tmp], { encoding: 'utf8', timeout: 60000 });
|
|
5432
|
+
const jsonCaught = rJson.status === 1 && /json:/.test(rJson.stdout);
|
|
5433
|
+
const pkg = JSON.parse(read(path.join(__dirname, '..', 'package.json')));
|
|
5434
|
+
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
|
+
return cleanOk && caught && synCaught && bomCaught && jsonCaught && wired;
|
|
5436
|
+
} finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
5376
5437
|
} }
|
|
5377
5438
|
];
|
|
5378
5439
|
}
|
package/lib/io.js
CHANGED
|
@@ -111,6 +111,9 @@ function _rescueCorruptJson(p, s) {
|
|
|
111
111
|
try { fs.renameSync(p, bak); console.error(`⚠ 손상 JSON 감지 — 원본 대피 후 저장: ${path.basename(bak)} (수동 복구용)`); } catch {}
|
|
112
112
|
}
|
|
113
113
|
function writeUtf8(p, s) {
|
|
114
|
+
// 1.36.65 (외부감사 F-08): 동일-내용 재기록 스킵 — 무변경 재설치/조정기의 mtime·diff churn 을 전역 해소.
|
|
115
|
+
// 내용이 완전히 같을 때만 생략하므로 의미 변화 없음(시각류 필드가 있으면 내용이 달라져 정상 기록).
|
|
116
|
+
try { if (fs.existsSync(p) && read(p) === String(s)) return; } catch {}
|
|
114
117
|
mkdirp(path.dirname(p));
|
|
115
118
|
_rescueCorruptJson(p, s);
|
|
116
119
|
const tmp = `${p}.tmp-${process.pid}-${++_writeSeq}`;
|
package/lib/pure-utils.js
CHANGED
|
@@ -738,8 +738,16 @@ function _looksSecretLike(value) {
|
|
|
738
738
|
|
|
739
739
|
// 1.9.367 (UR-0025): 라인 머지 순수 코어 — 기존 텍스트에 없는 라인만 append (substring 중복 방지). mergeLinesFile 의 I/O 분리.
|
|
740
740
|
function _mergeLines(currentText, lines) {
|
|
741
|
+
// 1.36.65 (검수 HIGH 파생, 선재 버그): substring 포함 검사는 '.env' 가 '.env.local' 에 흡수돼 영영 미복구 —
|
|
742
|
+
// 정확한 라인 단위 존재 검사로 교체.
|
|
741
743
|
let next = currentText || '';
|
|
742
|
-
|
|
744
|
+
const have = new Set(next.split(/\r?\n/).map(l => l.trim()).filter(Boolean));
|
|
745
|
+
for (const line of (lines || [])) {
|
|
746
|
+
const t = String(line).trim();
|
|
747
|
+
if (t && have.has(t)) continue;
|
|
748
|
+
next += (next.endsWith('\n') || !next ? '' : '\n') + line + '\n';
|
|
749
|
+
if (t) have.add(t);
|
|
750
|
+
}
|
|
743
751
|
return next;
|
|
744
752
|
}
|
|
745
753
|
// 1.9.367 (UR-0025): .env key-aware 머지 순수 코어 — 기존 KEY 값 보존(덮어쓰기 X), 신규 KEY/주석만 append. mergeEnvFile 의 I/O 분리.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.65",
|
|
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",
|
|
@@ -48,10 +48,11 @@
|
|
|
48
48
|
"LICENSE"
|
|
49
49
|
],
|
|
50
50
|
"scripts": {
|
|
51
|
-
"test": "node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e.js",
|
|
52
|
-
"test:core": "node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js",
|
|
53
|
-
"test:fast": "node ./bin/leerness.js selftest && node ./scripts/smoke.js",
|
|
51
|
+
"test": "node ./scripts/lint.js && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e.js",
|
|
52
|
+
"test:core": "node ./scripts/lint.js && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js",
|
|
53
|
+
"test:fast": "node ./scripts/lint.js && node ./bin/leerness.js selftest && node ./scripts/smoke.js",
|
|
54
54
|
"test:smoke": "node ./scripts/smoke.js",
|
|
55
|
+
"lint": "node ./scripts/lint.js",
|
|
55
56
|
"prepack": "node ./bin/leerness.js readme sync . && node ./bin/leerness.js --version"
|
|
56
57
|
},
|
|
57
58
|
"publishConfig": {
|
package/scripts/e2e.js
CHANGED
|
@@ -4953,7 +4953,11 @@ total++;
|
|
|
4953
4953
|
try {
|
|
4954
4954
|
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-arch-'));
|
|
4955
4955
|
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'ko', '--keep', '2'], { encoding: 'utf8', timeout: 30000 });
|
|
4956
|
-
|
|
4956
|
+
// 1.36.65: 무변경 migrate 는 no-op(백업 미생성) — 리텐션 검증 의도 유지를 위해 매회 변경 주입으로 스냅샷 강제
|
|
4957
|
+
for (let i = 0; i < 3; i++) {
|
|
4958
|
+
fs.appendFileSync(path.join(d, 'AGENTS.md'), `\nRETENTION PROBE ${i}\n`);
|
|
4959
|
+
cp.spawnSync(process.execPath, [CLI, 'migrate', d, '--keep', '2'], { encoding: 'utf8', timeout: 30000 });
|
|
4960
|
+
}
|
|
4957
4961
|
const adir = path.join(d, '.harness', 'archive');
|
|
4958
4962
|
const cnt = fs.readdirSync(adir).filter(n => /^leerness-/.test(n)).length;
|
|
4959
4963
|
fs.rmSync(d, { recursive: true, force: true });
|
|
@@ -7107,5 +7111,37 @@ total++;
|
|
|
7107
7111
|
if (!ok) failed++;
|
|
7108
7112
|
}
|
|
7109
7113
|
|
|
7114
|
+
// 1.36.65 (외부감사 F-08): no-op 재설치 — 변경 없으면 백업/아카이브/타임스탬프 무증식, 변경·구버전·force 는 정상 설치
|
|
7115
|
+
total++;
|
|
7116
|
+
{
|
|
7117
|
+
let ok = false;
|
|
7118
|
+
const _d = [];
|
|
7119
|
+
try {
|
|
7120
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-noop-')); _d.push(d);
|
|
7121
|
+
const R = () => cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--no-env'], { encoding: 'utf8', timeout: 40000 });
|
|
7122
|
+
R();
|
|
7123
|
+
const arch1 = fs.readdirSync(path.join(d, '.harness', 'archive')).length;
|
|
7124
|
+
const mf1 = fs.readFileSync(path.join(d, '.harness', 'manifest.json'), 'utf8');
|
|
7125
|
+
const r2 = R();
|
|
7126
|
+
const noopOk = r2.status === 0 && /no-op/.test(r2.stdout)
|
|
7127
|
+
&& fs.readdirSync(path.join(d, '.harness', 'archive')).length === arch1
|
|
7128
|
+
&& fs.readFileSync(path.join(d, '.harness', 'manifest.json'), 'utf8') === mf1;
|
|
7129
|
+
// 커스텀 추가 → 설치 수행(백업 생성) → 이후 다시 no-op + 커스텀 1회 유지
|
|
7130
|
+
fs.appendFileSync(path.join(d, 'AGENTS.md'), '\nNOOP CUSTOM LINE\n');
|
|
7131
|
+
const r3 = R();
|
|
7132
|
+
const changedOk = /backup created/.test(r3.stdout) && !/no-op/.test(r3.stdout);
|
|
7133
|
+
const r4 = R();
|
|
7134
|
+
const stableOk = /no-op/.test(r4.stdout) && (fs.readFileSync(path.join(d, 'AGENTS.md'), 'utf8').match(/NOOP CUSTOM LINE/g) || []).length === 1;
|
|
7135
|
+
// 구버전 마커 → 정상 설치
|
|
7136
|
+
fs.writeFileSync(path.join(d, '.harness', 'HARNESS_VERSION'), '1.36.60\n');
|
|
7137
|
+
const r5 = R();
|
|
7138
|
+
const verOk = /backup created/.test(r5.stdout);
|
|
7139
|
+
ok = noopOk && changedOk && stableOk && verOk;
|
|
7140
|
+
if (!ok) console.log(` [noop 디버그] noop=${noopOk} chg=${changedOk} stable=${stableOk} ver=${verOk}`);
|
|
7141
|
+
} catch (e) {} finally { _d.forEach(x => { try { fs.rmSync(x, { recursive: true, force: true }); } catch {} }); }
|
|
7142
|
+
console.log(ok ? '✓ B(1.36.65) F-08 no-op 재설치: 무변경=백업/타임스탬프 생략 · 변경/구버전=정상 설치 · 커스텀 1회 유지' : '✗ no-op 재설치 실패');
|
|
7143
|
+
if (!ok) failed++;
|
|
7144
|
+
}
|
|
7145
|
+
|
|
7110
7146
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
7111
7147
|
if (failed > 0) process.exit(1);
|
package/scripts/lint.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// scripts/lint.js — 0-deps 경량 lint 게이트 (1.36.64, 외부감사 F-10: lint/typecheck 부재)
|
|
2
|
+
// 범위: 배포되는 모든 .js 의 ① node --check 구문 ② 위험 문자(U+2028/U+2029 — 과거 raw U+2028 정규식 리터럴이
|
|
3
|
+
// 전 명령 SyntaxError 를 일으킨 실사고 재발 차단) ③ BOM ④ 저장소 .json 파싱 유효성.
|
|
4
|
+
// 빠르고(수 초) CI 친화(exit 1). npm run lint / test:fast 에 배선.
|
|
5
|
+
'use strict';
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const cp = require('child_process');
|
|
9
|
+
|
|
10
|
+
const ROOT = path.resolve(process.argv[2] || path.join(__dirname, '..')); // 테스트용 루트 오버라이드 허용
|
|
11
|
+
const TARGET_DIRS = ['bin', 'lib', 'scripts'];
|
|
12
|
+
const failures = [];
|
|
13
|
+
|
|
14
|
+
function listFiles(dir, ext) {
|
|
15
|
+
const out = [];
|
|
16
|
+
const walk = (d) => {
|
|
17
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
18
|
+
if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
|
|
19
|
+
const p = path.join(d, e.name);
|
|
20
|
+
if (e.isDirectory()) walk(p);
|
|
21
|
+
else if (e.name.endsWith(ext)) out.push(p);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
if (fs.existsSync(dir)) walk(dir);
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const jsFiles = TARGET_DIRS.flatMap(d => listFiles(path.join(ROOT, d), '.js'));
|
|
29
|
+
|
|
30
|
+
// ① 구문 (node --check)
|
|
31
|
+
for (const f of jsFiles) {
|
|
32
|
+
const r = cp.spawnSync(process.execPath, ['--check', f], { encoding: 'utf8', timeout: 30000 });
|
|
33
|
+
if (r.error) { failures.push(`syntax-check spawn failed: ${path.relative(ROOT, f)} — ${r.error.code || r.error.message}`); continue; } // (검수 #3) 스폰 실패를 구문오류와 구분
|
|
34
|
+
if (r.status !== 0) {
|
|
35
|
+
const lines = (r.stderr || '').split('\n');
|
|
36
|
+
const detail = lines.find(l => /Error/.test(l)) || lines[0] || ''; // (검수 #3) SyntaxError 본문 우선
|
|
37
|
+
failures.push(`syntax: ${path.relative(ROOT, f)} — ${detail.trim()}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ② 위험 문자 + ③ BOM
|
|
42
|
+
for (const f of jsFiles) {
|
|
43
|
+
const buf = fs.readFileSync(f);
|
|
44
|
+
if (buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF) failures.push(`bom: ${path.relative(ROOT, f)}`);
|
|
45
|
+
const text = buf.toString('utf8');
|
|
46
|
+
const ls = text.indexOf('\u2028'); const ps = text.indexOf('\u2029'); // 이스케이프 표기 — 자기참조 트랩 방지
|
|
47
|
+
if (ls >= 0) failures.push(`U+2028 line-separator: ${path.relative(ROOT, f)}:${text.slice(0, ls).split('\n').length} (과거 전-명령 SyntaxError 실사고 문자)`);
|
|
48
|
+
if (ps >= 0) failures.push(`U+2029 paragraph-separator: ${path.relative(ROOT, f)}:${text.slice(0, ps).split('\n').length}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ④ 저장소 JSON 유효성 (배포 관련 루트 + data 성격 파일)
|
|
52
|
+
const jsonFiles = ['package.json', ...listFiles(path.join(ROOT, 'lib'), '.json'), ...listFiles(path.join(ROOT, 'scripts'), '.json')].map(f => path.isAbsolute(f) ? f : path.join(ROOT, f));
|
|
53
|
+
for (const f of jsonFiles) {
|
|
54
|
+
if (!fs.existsSync(f)) continue;
|
|
55
|
+
try { JSON.parse(fs.readFileSync(f, 'utf8').replace(/^/, '')); } catch (e) { failures.push(`json: ${path.relative(ROOT, f)} — ${e.message.slice(0, 80)}`); }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (failures.length) {
|
|
59
|
+
console.log(`✗ lint: ${failures.length} issue(s)`);
|
|
60
|
+
failures.forEach(x => console.log(' - ' + x));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
console.log(`✓ lint: ${jsFiles.length} js (syntax/U+2028/BOM) + ${jsonFiles.length} json — clean`);
|