leerness 1.36.19 → 1.36.21
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 +21 -0
- package/README.md +4 -4
- package/bin/leerness.js +108 -38
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.21 — 2026-07-14 — 경로/EOL/형태 견고성 4종 — 없는 경로에 트리 생성·CRLF plan tasks 증발·오타 하위명령 전체설치·user-requests 원본 덮어쓰기 (22 에이전트 전수 sweep, 전부 재현확정)
|
|
4
|
+
|
|
5
|
+
codex 이연분(#8/#6) 재현에 그치지 않고 **동일 버그 클래스를 전수 조사**(CRLF 파서 / `--json` 실패경로 / 로더 스키마 / mutation-order 잔여) — 22 에이전트가 실행 기반으로 확정하고 후보마다 독립 재검증. 확정 P2 중 **현실성 있는 4건**만 채택(P3/contrived 는 기각·이연).
|
|
6
|
+
|
|
7
|
+
- **handoff / env detect 가 없는 경로에 디렉토리 트리 + `.harness` 를 생성 (P2, 현실성 high)**: 오타 경로로 `handoff` 하면 쓰레기 트리가 생기고, `--json` 은 `ok`/`error` 없는 **정상 "빈 프로젝트" JSON + exit 0** 이라 기계 소비자가 오타를 감지 못 했음. status/health/drift/lazy detect/scan secrets/session close 는 이미 `path_not_found` + exit 1(UR-0136)인데 **이 둘만 관례 갭**. → 두 진입점에 존재 가드(`--pulse`/`--all-apps` 분기보다 앞). 실측: 없는 경로 → exit 1 + `path_not_found` + **디렉토리 생성 0**, 기존 경로 무회귀.
|
|
8
|
+
- **CRLF `plan.md` 에서 모든 마일스톤 tasks 가 조용히 증발 (P2, codex #8a — 3 에이전트 독립 수렴)**: 체크박스 정규식이 `/m` 없이 후행 `\r` 앞에서 `$` 를 못 잡아 실패. id/title/status/progress 는 `/m` 이라 정상 파싱돼 **부분·무증상 실패**(`plan list` 의 Tasks 줄 소실, MCP `leerness_plan_list` 가 tasks 0건 보고 → 계약 위반). leerness 엔 체크박스를 켜는 명령이 없어 tasks 사용엔 외부 편집이 필수인데 Windows 관용 편집(PowerShell `Set-Content` 등)이 기본 CRLF → 현실 경로. → 코드베이스 기존 관례대로 **읽는 즉시 정규화**(혼합 EOL 도 복구). 실측 LF/CRLF 모두 tasks 2건.
|
|
9
|
+
- **`migrate` 오타/추측 하위명령이 전체 설치를 수행 (P2)**: `migrate aduit` / `migrate check` 가 그 토큰을 **경로로 삼아** 오타명 디렉토리를 만들고 77개 파일을 설치 + `--json` 무시 + exit 0. → **존재 기반 가드**(migrate 의 전제는 기존 프로젝트, 새 트리 생성은 `init` 의 일) + `unknown_subcommand` 구조화 에러. 정당 흐름(`migrate .` / `migrate /existing` / `migrate --path X` / `migrate audit|apply|plan`)은 전부 보존. 실측: 오타 → exit 1 + **디렉토리 생성 0**.
|
|
10
|
+
- **`user-requests.json` 미인식 형태 → 원본 통째 덮어쓰기 (P2, 데이터손실)**: wrapper 가 아닌 유효 JSON 을 조용히 `{requests:[]}` 로 강제한 뒤, mutation 이 **읽지 못한 원본을 새 배열로 대체**(read-modify-write 파괴). → bare array 는 관대 수용해 자기치유(decisions/lessons 관례), 그 외 미인식/파싱실패는 `unrecognized` 로 표시해 **3개 파괴적 쓰기 경로가 모두 bail**. 실측: 미인식 형태에서 `requests add` → 기록 건너뜀 + **파일 INTACT**, bare array → 보존+append, canonical 무회귀. 부수 수정: id 를 `length+1` 로 만들어 200개 truncate 후 충돌하던 것 → **최대 UR 번호+1** 파생.
|
|
11
|
+
- **검증**: selftest 291/291(신규 4수정 소스가드 + CRLF 판별 순수행위), 4건 전부 임시 워크스페이스에서 재현→수정 후 재검(17 체크 중 14→17 통과, 나머지 3은 테스트 스크립트 결함으로 판명 후 정정), 게이트 e2e, 게시본 클린룸.
|
|
12
|
+
- **기각/이연**: `[null]`·필드누락 JSON crash(P3, low — leerness 자체 파서는 null 을 만들지 않아 수동 손상 필요) · `copyRec` 심링크(P2, low — .harness 에 심링크를 둘 이유 없음, Windows 권한상 재현 불가) · `--json` 진행로그 오염 12건 중 P3 다수 · **UR-0027 확장 증거**(`requests` 계열은 positional 경로 미지원 → `requests add "text" /other/project` 가 조용히 cwd 에 기록. `task` 는 지원해 불일치가 실수를 유도 — 실제 피해 재현. 계약 변경 리스크로 별도 라운드).
|
|
13
|
+
|
|
14
|
+
## 1.36.20 — 2026-07-14 — `--apply --json`이 실제로 적용하도록 수정 — mutation-before-output (codex fresh-QA #7, 3커맨드 재현확정)
|
|
15
|
+
|
|
16
|
+
codex fresh-surface QA(1.36.19 이연분)에서 확정한 **silent no-op** 버그 수정: `--json` 분기가 mutation "전"에 return 해, `--apply --json`이 적용을 보고하고도 실제로는 아무 변경도 안 하던 3개 커맨드.
|
|
17
|
+
|
|
18
|
+
- **`reuse autodetect --apply --json`** — 후보만 보고하고 reuse-map.md를 안 씀(bin:20307의 --json return이 20319의 --apply 앞). → apply를 출력 전에 수행하고 JSON에 `applied`/`added` 반영. 실측: `--apply --json` → `applied:true, added:2` + reuse-map 실제 기록(0→1건), `--apply`(no-json) 무회귀.
|
|
19
|
+
- **`release cleanup --apply --json`** — `apply:true, deleteCount:N`을 보고하고도 브랜치를 안 지움(15018의 --json return이 15051의 삭제 앞). → 삭제를 출력 전에 수행하고 JSON에 `deleted`/`deleteFailed` 반영 + 삭제 실패 시 `exitCode=1`. 실측: `--apply --keep 1 --json` → `deleted:3` + 브랜치 실제 삭제(4→1).
|
|
20
|
+
- **`env encoding --apply --json`** — read-only 파일 apply 실패를 `action:'failed'`로 기록만 하고 exit 0. → 실패 시 `process.exitCode=1`. 실측: read-only→exit 1, writable→exit 0.
|
|
21
|
+
- **검증**: selftest 290/290(신규 소스가드 — 3커맨드의 apply-before-output 구조 + 실패 exit 반영), 각 커맨드 재현확정(임시 프로젝트/git-repo/read-only 파일), 게이트 e2e, 게시본 클린룸.
|
|
22
|
+
- **이연(다음 라운드)**: codex #8(CRLF `plan.md` 태스크 파서 tasks:[] + 잘못된 JSON 스키마가 `--json` 경로 crash — 파서 서브시스템 별도 조사) + #6(`copyRec` lstatSync 심링크).
|
|
23
|
+
|
|
3
24
|
## 1.36.19 — 2026-07-14 — 전략 앵커(project-brief/plan Goal) 미작성 감지 — 인계 AI가 프로젝트 맥락을 못 받던 근본원인 표면화 (실사용 7 프로젝트 dogfood)
|
|
4
25
|
|
|
5
26
|
**현장 관찰**: leerness를 오래 적용한 실프로젝트에서, 인계받은 AI(codex 등)가 "leerness를 참조하나 프로젝트 맥락을 이해 못 하고" 작업하는 현상이 보고됨. 7개 실사용 프로젝트를 dogfood 조사한 결과 근본원인이 드러남 — **동적 상태(current-state 7/7, decisions)는 잘 유지되나, 정체성 앵커인 `project-brief.md` Purpose(5/7 미작성)와 `plan.md` Goal/Scope(7/7 미작성)이 템플릿 placeholder 그대로 방치됨**. 27개 결정을 쌓은 프로젝트조차 brief는 빈칸. 그 결과 인계 AI는 "최근 무슨 작업을 했는지"(동적)는 받지만 "이 프로젝트가 무엇이고 범위가 어디까지인지"(정체성)를 못 받아, 최근 스레드만 이어가며 전체 프레임을 놓침. 게다가 leerness 스스로 이 갭을 전혀 경고하지 않았음(감지 부재).
|
package/README.md
CHANGED
|
@@ -125,7 +125,7 @@ MIT
|
|
|
125
125
|
<!-- leerness:project-readme:start -->
|
|
126
126
|
## Leerness Project Harness
|
|
127
127
|
|
|
128
|
-
이 프로젝트는 Leerness v1.36.
|
|
128
|
+
이 프로젝트는 Leerness v1.36.21 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
129
129
|
|
|
130
130
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
131
131
|
|
|
@@ -179,7 +179,7 @@ leerness memory restore decision <date|title>
|
|
|
179
179
|
|
|
180
180
|
### MCP server (외부 AI 통합)
|
|
181
181
|
|
|
182
|
-
Leerness v1.36.
|
|
182
|
+
Leerness v1.36.21는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
|
|
183
183
|
|
|
184
184
|
```jsonc
|
|
185
185
|
// 카테고리별
|
|
@@ -200,7 +200,7 @@ Leerness v1.36.19는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
200
200
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
201
201
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push (1.9.140+) → 7) session close → 8) 다음 라운드 예약.
|
|
202
202
|
|
|
203
|
-
현재 누적: **70 라운드 (1.9.40 → 1.36.
|
|
203
|
+
현재 누적: **70 라운드 (1.9.40 → 1.36.21)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
204
204
|
|
|
205
205
|
### 성능 가이드 (1.9.140 측정)
|
|
206
206
|
|
|
@@ -238,6 +238,6 @@ leerness release pack --close --auto-main-push
|
|
|
238
238
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
239
239
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
240
240
|
|
|
241
|
-
Last synced by Leerness v1.36.
|
|
241
|
+
Last synced by Leerness v1.36.21: 2026-07-15
|
|
242
242
|
<!-- leerness:project-readme:end -->
|
|
243
243
|
|
package/bin/leerness.js
CHANGED
|
@@ -33,7 +33,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
|
|
|
33
33
|
const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_CATALOG, _TOOL_CATALOG, _LSP_LANG_PATTERNS, OPTIMISM_PATTERNS, BUILT_IN_PERSONAS, STRINGS, BUILTIN_CATALOG, ROADMAP_STATUS_LABEL, ROADMAP_STATUS_COLOR, SECRET_PATTERNS, MERGE_OVERWRITE_FILES, MINIMAL_SKIP_KEYS, REQUIRED_WORKSPACE_FILES, KEYWORD_STOPWORDS, SKILL_CATALOG_PRESETS } = require('../lib/catalogs'); // 1.9.344/368/369 (UR-0025): catalog 분리 · 1.11.4 (UR-0007): _TOOL_CATALOG
|
|
34
34
|
const { findCorruptedStateJson: _findCorruptedStateJson } = require('../lib/state-integrity'); // 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태 무결성 (audit/health/check 공유)
|
|
35
35
|
|
|
36
|
-
const VERSION = '1.36.
|
|
36
|
+
const VERSION = '1.36.21';
|
|
37
37
|
|
|
38
38
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
39
39
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -1923,13 +1923,19 @@ function _analyzeWakeupStatus(root) {
|
|
|
1923
1923
|
// 레코드 구조: { id, text, recordedAt, status: 'open'|'in-progress'|'completed'|'dropped',
|
|
1924
1924
|
// linkedTaskIds?, linkedPlanIds?, linkedDecisionIds?, notes?, completedAt?, droppedAt? }
|
|
1925
1925
|
function _userRequestsPath(root) { return path.join(root, '.harness', 'user-requests.json'); }
|
|
1926
|
+
// 1.36.21 (전수 sweep P2, 데이터손실): 종전엔 wrapper 가 아닌 "유효 JSON" 을 조용히 {requests:[]} 로 강제하고, 이후 mutation 이
|
|
1927
|
+
// 파일 전체를 새 배열로 덮어써 원본을 파괴했다(읽지 못한 데이터를 지우는 read-modify-write). 이제 (1) bare array 는 관대 수용해
|
|
1928
|
+
// 다음 쓰기에 자기치유(decisions/lessons 관례와 동일), (2) 그 외 미인식 형태/파싱 실패는 unrecognized 로 표시해 쓰기 경로가 bail.
|
|
1929
|
+
// 읽기 전용 소비자들은 .requests 만 구조분해하므로 하위호환.
|
|
1926
1930
|
function _loadUserRequests(root) {
|
|
1927
1931
|
try {
|
|
1928
1932
|
const fp = _userRequestsPath(root);
|
|
1929
|
-
if (!exists(fp)) return { requests: [] };
|
|
1933
|
+
if (!exists(fp)) return { requests: [] }; // 미존재 = 신규 시작(쓰기 허용)
|
|
1930
1934
|
const j = JSON.parse(read(fp));
|
|
1931
|
-
|
|
1932
|
-
|
|
1935
|
+
if (j && Array.isArray(j.requests)) return { requests: j.requests }; // canonical wrapper
|
|
1936
|
+
if (Array.isArray(j)) return { requests: j }; // bare array → 관대 수용(다음 쓰기에 wrapper 로 치유)
|
|
1937
|
+
return { requests: [], unrecognized: true }; // 미인식 형태 → 파괴적 쓰기 차단
|
|
1938
|
+
} catch { return { requests: [], unrecognized: true }; } // 파싱 실패 → 차단(덮어쓰기 방지)
|
|
1933
1939
|
}
|
|
1934
1940
|
function _writeUserRequests(root, requests) {
|
|
1935
1941
|
try {
|
|
@@ -1941,11 +1947,14 @@ function _writeUserRequests(root, requests) {
|
|
|
1941
1947
|
function _recordUserRequest(root, text, opts = {}) {
|
|
1942
1948
|
if (!text || typeof text !== 'string') return null;
|
|
1943
1949
|
const state = _loadUserRequests(root);
|
|
1950
|
+
if (state.unrecognized) { warn(`user-requests.json 형태 인식 불가 — 기록 건너뜀(원본 덮어쓰기 방지). 파일 확인: ${_userRequestsPath(root)}`); return null; } // 1.36.21: 읽지 못한 데이터 파괴 차단
|
|
1944
1951
|
// 중복 방지: 동일 텍스트 + open 상태 이미 존재 시 skip
|
|
1945
1952
|
const norm = text.trim().slice(0, 500);
|
|
1946
|
-
const dup = state.requests.find(r => r.text === norm && (r.status === 'open' || r.status === 'in-progress'));
|
|
1953
|
+
const dup = state.requests.find(r => r && r.text === norm && (r.status === 'open' || r.status === 'in-progress'));
|
|
1947
1954
|
if (dup) return dup;
|
|
1948
|
-
|
|
1955
|
+
// 1.36.21 (전수 sweep 부수발견): id 를 length+1 로 만들면 200개 초과 truncate(slice(-200)) 후 기존 id 와 충돌 → 최대 UR 번호 +1 로 파생.
|
|
1956
|
+
const _maxUr = state.requests.reduce((mx, r) => { const m = /^UR-(\d{4,})$/.exec(String((r && r.id) || '')); return m ? Math.max(mx, parseInt(m[1], 10)) : mx; }, 0);
|
|
1957
|
+
const id = 'UR-' + String(_maxUr + 1).padStart(4, '0');
|
|
1949
1958
|
const entry = {
|
|
1950
1959
|
id, text: norm, recordedAt: new Date().toISOString(),
|
|
1951
1960
|
status: opts.status || 'open',
|
|
@@ -1962,7 +1971,8 @@ function _recordUserRequest(root, text, opts = {}) {
|
|
|
1962
1971
|
}
|
|
1963
1972
|
function _updateUserRequest(root, id, patch) {
|
|
1964
1973
|
const state = _loadUserRequests(root);
|
|
1965
|
-
|
|
1974
|
+
if (state.unrecognized) { warn(`user-requests.json 형태 인식 불가 — 업데이트 건너뜀(원본 덮어쓰기 방지). 파일 확인: ${_userRequestsPath(root)}`); return null; } // 1.36.21
|
|
1975
|
+
const idx = state.requests.findIndex(r => r && r.id === id);
|
|
1966
1976
|
if (idx < 0) return null;
|
|
1967
1977
|
state.requests[idx] = { ...state.requests[idx], ...patch, updatedAt: new Date().toISOString() };
|
|
1968
1978
|
if (patch.status === 'completed' && !state.requests[idx].completedAt) state.requests[idx].completedAt = new Date().toISOString();
|
|
@@ -2611,6 +2621,8 @@ function envCmd(root, sub) {
|
|
|
2611
2621
|
}
|
|
2612
2622
|
}
|
|
2613
2623
|
}
|
|
2624
|
+
// 1.36.20 (codex fresh-QA #7): apply 실패(read-only EPERM 등)를 exit code 에 반영 — 종전엔 action:'failed' 를 기록만 하고 exit 0.
|
|
2625
|
+
if (apply && (result.applied || []).some(a => a.action === 'failed')) process.exitCode = 1;
|
|
2614
2626
|
if (has('--json')) { log(JSON.stringify(result, null, 2)); return; }
|
|
2615
2627
|
log(cy(`# leerness env encoding-check (1.9.241/242, UR-0014) — 셸 스크립트 인코딩 위험 감지`));
|
|
2616
2628
|
log('');
|
|
@@ -2963,6 +2975,34 @@ function _selfTestCases() {
|
|
|
2963
2975
|
const realDecl = p._parseContractSpec('function realFn(){}\n').declared.includes('realFn'); // 회귀: 펜스 밖 선언 유지
|
|
2964
2976
|
return bracket && dotStill && fenceExcluded && realDecl;
|
|
2965
2977
|
} },
|
|
2978
|
+
{ name: 'apply-before-output mutation 순서 (1.36.20, codex fresh-QA #7): reuse/release --apply 가 --json return 전 수행 + 실패 exit code — 소스가드', run: () => {
|
|
2979
|
+
const s = read(__filename);
|
|
2980
|
+
const reuseOk = s.includes('project: path.basename(root), found, applied, added') && s.includes("failJson(has('--json'), 'no_reuse_map'"); // reuse: apply 먼저 → json 에 applied/added
|
|
2981
|
+
const releaseOk = s.includes('deleteCount: toDelete.length, deleted, deleteFailed') && s.includes('if (deleteFailed > 0) process.exitCode = 1'); // release: 삭제 먼저 → json 에 deleted
|
|
2982
|
+
const encOk = s.includes("(result.applied || []).some(a => a.action === 'failed')) process.exitCode = 1"); // encoding: 실패 exit1
|
|
2983
|
+
return reuseOk && releaseOk && encOk;
|
|
2984
|
+
} },
|
|
2985
|
+
{ name: '경로/EOL/형태 견고성 4종 (1.36.21, 전수 sweep P2): handoff·env detect 없는경로 가드 + CRLF plan tasks + migrate 오타설치 차단 + user-requests 미인식 쓰기차단 — 소스가드 + 순수 행위', run: () => {
|
|
2986
|
+
const s = read(__filename);
|
|
2987
|
+
// B: 없는 경로에 트리/.harness 를 만들고 빈 프로젝트로 보고하던 것 차단 (UR-0136 관례 정렬)
|
|
2988
|
+
const handoffGuard = s.includes("if (!exists(_hr) || !fs.statSync(_hr).isDirectory()) { failJson(has('--json'), 'path_not_found'");
|
|
2989
|
+
const envGuard = s.includes("if (!exists(root) || !fs.statSync(root).isDirectory()) { failJson(has('--json') || opts.json, 'path_not_found'");
|
|
2990
|
+
// A: CRLF plan → tasks 증발 (읽는 즉시 정규화)
|
|
2991
|
+
const planNorm = s.includes("const text = read(pp).replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');");
|
|
2992
|
+
// C: migrate 오타/추측 하위명령이 그 토큰을 경로로 삼아 전체 설치하던 것 차단
|
|
2993
|
+
const migrateGuard = s.includes("failJson(has('--json'), 'unknown_subcommand'") && s.includes('_msub');
|
|
2994
|
+
// D: user-requests 미인식 형태를 조용히 비우고 덮어쓰던 데이터손실 차단 + id 충돌
|
|
2995
|
+
const urTolerant = s.includes('if (Array.isArray(j)) return { requests: j };') && s.includes('return { requests: [], unrecognized: true };');
|
|
2996
|
+
const urWriteGuard = (s.match(/state\.unrecognized/g) || []).length >= 2 && s.includes('ur.unrecognized');
|
|
2997
|
+
const urMaxId = s.includes("/^UR-(\\d{4,})$/.exec");
|
|
2998
|
+
// 순수 행위(실 흐름 복제: 정규화 → split('\n') → 줄별 매치). \r 가 남으면 앵커 $ 가 실패해 태스크 0건, 정규화하면 2건.
|
|
2999
|
+
const crlfBlock = '- [ ] one\r\n- [x] two\r\n';
|
|
3000
|
+
const hit = ls => ls.filter(l => /^-\s*\[([\sx])\]\s*(.+)$/.test(l)).length;
|
|
3001
|
+
const rawHit = hit(crlfBlock.split('\n')); // 종전 동작: 0 (후행 \r)
|
|
3002
|
+
const normHit = hit(crlfBlock.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n')); // 수정 후: 2
|
|
3003
|
+
const crlfBehavior = rawHit === 0 && normHit === 2;
|
|
3004
|
+
return handoffGuard && envGuard && planNorm && migrateGuard && urTolerant && urWriteGuard && urMaxId && crlfBehavior;
|
|
3005
|
+
} },
|
|
2966
3006
|
{ name: 'ID 리더 5자리+ 대응 (1.36.18, UR-0052 P1-2): \\d{4}→\\d{4,} — 10k+ ID(T-10000) truncation 방지, 날짜 연도는 exactly-4 보존 — 행위검사', run: () => {
|
|
2967
3007
|
// 행위: 5자리 ID 전체 캡처(구 \d{4}는 앞 4자리만 → truncation/충돌), 4자리 ID 무회귀, 날짜 연도 exactly-4 유지.
|
|
2968
3008
|
const taskRe = /\bT-(\d{4,})\b/;
|
|
@@ -3093,7 +3133,7 @@ function _selfTestCases() {
|
|
|
3093
3133
|
{ name: 'UR-0068(외부리뷰 P2): _roadmapParseMilestones 블록 경계 — 다음 milestone status 누출 차단', run: () => { const m = require('../lib/pure-utils'); const r = m._roadmapParseMilestones('### M-0001. A\n\n### M-0002. B\nStatus: done\nProgress: 80%\n'); return r.length === 2 && r[0].status === 'planned' && r[0].progress === 0 && r[1].status === 'done' && r[1].progress === 80; } },
|
|
3094
3134
|
{ name: 'UR-0066(외부리뷰 P2): shell:true 주입 가드 — agents bench task _shellQuoteArg + fetchNpmLatest cmd.exe args', run: () => { const m = require('../lib/pure-utils'); const src = read(__filename); const agentsSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'agents.js')); const benchQuoted = agentsSrc.includes('const qTask = ' + '_shellQuoteArg(task)'); const npmSafe = /'\/d', '\/s', '\/c', 'npm', 'view'/.test(src); const q = m._shellQuoteArg('a & b'); const safe = (process.platform === 'win32' ? q === '"a & b"' : q === "'a & b'"); return benchQuoted && npmSafe && safe; } },
|
|
3095
3135
|
{ name: 'UR-0072(외부리뷰 P3): compareVer pre-release + _classifyCJK 한자 kana 귀속', run: () => { const m = require('../lib/pure-utils'); const verOk = m.compareVer('1.9.0-beta', '1.9.0') === -1 && m.compareVer('1.9.0', '1.9.0-beta') === 1 && m.compareVer('1.9.5', '1.9.5') === 0 && m.compareVer('1.9.6', '1.9.5') === 1; const jp = Buffer.from([0xE3, 0x81, 0x82, 0xE6, 0x97, 0xA5, 0xE6, 0x9C, 0xAC]); const cn = Buffer.from([0xE4, 0xB8, 0xAD, 0xE5, 0x9B, 0xBD]); const rj = m._classifyCJK(jp, jp.length); const rc = m._classifyCJK(cn, cn.length); const cjkOk = rj.japanese > rj.chinese && rc.chinese > 0 && rc.japanese === 0; return verOk && cjkOk; } },
|
|
3096
|
-
{ name: 'UR-0075 Phase A: 마이그레이션 가이드(_migrationGuideText) + migrate --guide 와이어 + init/migrate/update --path', run: () => { const m = require('../lib/pure-utils'); const g = m._migrationGuideText('1.9.355'); const guideOk = typeof g === 'string' && g.includes('마이그레이션 가이드') && g.includes('update --check --path') && g.includes('selftest') && g.includes('canonical JSON') && g.includes('롤백') && g.includes('1.9.355'); const src = read(__filename); const wired = src.includes("has('--guide') || args[1] === " + "'guide'") && src.includes('install(arg(' + "'--path',
|
|
3136
|
+
{ name: 'UR-0075 Phase A: 마이그레이션 가이드(_migrationGuideText) + migrate --guide 와이어 + init/migrate/update --path', run: () => { const m = require('../lib/pure-utils'); const g = m._migrationGuideText('1.9.355'); const guideOk = typeof g === 'string' && g.includes('마이그레이션 가이드') && g.includes('update --check --path') && g.includes('selftest') && g.includes('canonical JSON') && g.includes('롤백') && g.includes('1.9.355'); const src = read(__filename); const wired = src.includes("has('--guide') || args[1] === " + "'guide'") && src.includes('install(arg(' + "'--path', _msub || process.cwd())") && src.includes('updateCmd(arg(' + "'--path', args[1] || process.cwd())"); return guideOk && wired; } }, // 1.36.21: migrate 는 존재가드 도입으로 args[1]→_msub (--path 와이어는 동일)
|
|
3097
3137
|
{ name: 'UR-0075 Phase B: migrate audit(dry-run 스키마 drift) 명령 + 와이어', run: () => { const src = read(__filename); return typeof migrateAuditCmd === 'function' && src.includes('migrateAuditCmd(arg(' + "'--path'") && src.includes("args[1] === " + "'audit'"); } },
|
|
3098
3138
|
{ name: 'UR-0075 Phase C: migrate apply(canonical 백필 비파괴 적용) 명령 + 와이어', run: () => { const src = read(__filename); return typeof migrateApplyCmd === 'function' && src.includes('migrateApplyCmd(arg(' + "'--path'") && src.includes("args[1] === " + "'apply'"); } },
|
|
3099
3139
|
{ name: 'UR-0075 Phase D: migrate plan(임시폴더 설치 후 비교) 명령 + 와이어', run: () => { const src = read(__filename); return typeof migratePlanCmd === 'function' && src.includes('migratePlanCmd(arg(' + "'--path'") && src.includes("args[1] === " + "'plan'"); } },
|
|
@@ -6960,10 +7000,11 @@ function _autoFixIdempotency(root) {
|
|
|
6960
7000
|
// 2) user-requests.json open 중복 — 동일 텍스트 중 최초만 open 유지, 나머지 dropped(보존)
|
|
6961
7001
|
try {
|
|
6962
7002
|
const ur = _loadUserRequests(root);
|
|
7003
|
+
if (ur.unrecognized) throw new Error('user-requests.json 형태 인식 불가 — dedup 건너뜀(원본 덮어쓰기 방지)'); // 1.36.21: 3번째 파괴적 caller 도 차단
|
|
6963
7004
|
const seen = new Map();
|
|
6964
7005
|
let droppedReq = 0;
|
|
6965
7006
|
for (const r of ur.requests) {
|
|
6966
|
-
if (r.status !== 'open' && r.status !== 'in-progress') continue;
|
|
7007
|
+
if (!r || (r.status !== 'open' && r.status !== 'in-progress')) continue;
|
|
6967
7008
|
const k = (r.text || '').trim();
|
|
6968
7009
|
if (seen.has(k)) { r.status = 'dropped'; r.droppedReason = `auto-dedup-1.9.293 (== ${seen.get(k)})`; droppedReq++; }
|
|
6969
7010
|
else seen.set(k, r.id);
|
|
@@ -7665,7 +7706,12 @@ function planListCmd(root, opts = {}) {
|
|
|
7665
7706
|
}
|
|
7666
7707
|
return ok('plan.md 없음 — leerness plan add "<text>" 로 첫 milestone 등록');
|
|
7667
7708
|
}
|
|
7668
|
-
|
|
7709
|
+
// 1.36.21 (codex #8a, 전수 sweep 3 에이전트 수렴): CRLF plan.md 에서 모든 마일스톤의 tasks 가 조용히 [] 로 증발하던 것 수정.
|
|
7710
|
+
// 체크박스 정규식(/^-\s*\[([\sx])\]\s*(.+)$/)이 /m 없이 후행 \r 앞에서 $ 를 못 잡아 실패 — id/title/status/progress 는 /m 이라 정상 파싱되어
|
|
7711
|
+
// 부분·무증상 실패였음(plan list 의 Tasks 줄 소실, MCP leerness_plan_list 가 tasks 0건 보고 → 계약 위반). leerness 는 체크박스를 켜는 명령이
|
|
7712
|
+
// 없어 tasks 사용엔 외부 편집이 필수인데, Windows 관용 편집(PowerShell Set-Content 등)이 기본 CRLF 라 현실 경로. 코드베이스 기존 관례(:4643)대로
|
|
7713
|
+
// 읽는 즉시 정규화 — 혼합 EOL(기존 CRLF + plan add 의 LF append)도 함께 복구되고 이 함수의 향후 정규식 재발도 차단.
|
|
7714
|
+
const text = read(pp).replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
7669
7715
|
const milestones = [];
|
|
7670
7716
|
// ### M-XXXX. <title> 블록 추출
|
|
7671
7717
|
const blocks = text.split(/\n(?=### M-\d{4,}\.)/);
|
|
@@ -10656,6 +10702,12 @@ function _handoffWorkspace(rootBase) {
|
|
|
10656
10702
|
}
|
|
10657
10703
|
|
|
10658
10704
|
function handoffCmd(root) {
|
|
10705
|
+
// 1.36.21 (전수 sweep P2/high): 존재하지 않는 경로에 디렉토리 트리 + .harness 를 "생성"하고 깨끗한 빈 프로젝트로 보고하던 것 차단.
|
|
10706
|
+
// 오타 경로에 handoff 하면 쓰레기 트리가 생기고, --json 은 ok/error 없는 정상 JSON + exit 0 이라 기계 소비자가 오타를 감지 못 했음.
|
|
10707
|
+
// status/health/drift/lazy detect/scan secrets/session close 는 이미 path_not_found + exit 1(UR-0136) — handoff/env detect 만 누락된 관례 갭.
|
|
10708
|
+
// --pulse/--all-apps 분기보다 "앞"에 둬야 전 모드가 커버됨.
|
|
10709
|
+
const _hr = absRoot(root);
|
|
10710
|
+
if (!exists(_hr) || !fs.statSync(_hr).isDirectory()) { failJson(has('--json'), 'path_not_found', `경로 없음 또는 디렉토리 아님: ${_hr}`); return; }
|
|
10659
10711
|
// 1.9.232: --pulse 옵션 — pulse 1 line 형식으로 출력 (handoff 전체 대신)
|
|
10660
10712
|
if (has('--pulse')) {
|
|
10661
10713
|
return pulseCmd(absRoot(root));
|
|
@@ -15014,11 +15066,21 @@ function releaseCleanupCmd(root) {
|
|
|
15014
15066
|
});
|
|
15015
15067
|
const recent = mergedBranches.slice(0, keep);
|
|
15016
15068
|
const toDelete = mergedBranches.slice(keep).filter(b => b !== currentBranch);
|
|
15069
|
+
// 1.36.20 (codex fresh-QA #7): 삭제(mutation)를 출력(json/human) "전"에 수행. 종전엔 --json 분기가 삭제 전 return 해
|
|
15070
|
+
// `release cleanup --apply --json` 이 apply:true·deleteCount N 을 보고하고도 실제로 아무것도 안 지우던 silent no-op.
|
|
15071
|
+
let deleted = 0, deleteFailed = 0;
|
|
15072
|
+
if (apply) {
|
|
15073
|
+
for (const b of toDelete) {
|
|
15074
|
+
const r = cp.spawnSync('git', ['branch', '-d', b], { cwd: root, encoding: 'utf8' });
|
|
15075
|
+
if (r.status === 0) deleted++; else deleteFailed++;
|
|
15076
|
+
}
|
|
15077
|
+
if (deleteFailed > 0) process.exitCode = 1; // 실패 시 nonzero exit
|
|
15078
|
+
}
|
|
15017
15079
|
// 5) JSON
|
|
15018
15080
|
if (has('--json')) {
|
|
15019
15081
|
log(JSON.stringify({
|
|
15020
15082
|
apply, keep, total: allBranches.length, merged: mergedBranches.length,
|
|
15021
|
-
unmerged: unmergedBranches.length, deleteCount: toDelete.length,
|
|
15083
|
+
unmerged: unmergedBranches.length, deleteCount: toDelete.length, deleted, deleteFailed,
|
|
15022
15084
|
toDelete, recent: recent.slice(0, keep), unmergedSample: unmergedBranches.slice(0, 5)
|
|
15023
15085
|
}, null, 2));
|
|
15024
15086
|
return;
|
|
@@ -15049,13 +15111,7 @@ function releaseCleanupCmd(root) {
|
|
|
15049
15111
|
if (recent.length > 5) log(dm(` ... +${recent.length - 5}개`));
|
|
15050
15112
|
log('');
|
|
15051
15113
|
if (apply) {
|
|
15052
|
-
|
|
15053
|
-
for (const b of toDelete) {
|
|
15054
|
-
const r = cp.spawnSync('git', ['branch', '-d', b], { cwd: root, encoding: 'utf8' });
|
|
15055
|
-
if (r.status === 0) ok++;
|
|
15056
|
-
else fail++;
|
|
15057
|
-
}
|
|
15058
|
-
log(gr(` ✓ 삭제 완료: ${ok}/${toDelete.length}건`) + (fail > 0 ? rd(` · 실패 ${fail}`) : ''));
|
|
15114
|
+
log(gr(` ✓ 삭제 완료: ${deleted}/${toDelete.length}건`) + (deleteFailed > 0 ? rd(` · 실패 ${deleteFailed}`) : '')); // 1.36.20: 삭제는 출력 전 수행됨(#7)
|
|
15059
15115
|
} else {
|
|
15060
15116
|
log(dm(` → 적용: leerness release cleanup --apply (안전: merged만 삭제, 현재 branch 보호)`));
|
|
15061
15117
|
log(dm(` → --keep N: 최근 N개 유지 (default 5)`));
|
|
@@ -17380,6 +17436,8 @@ function _diffEnvSnapshots(prev, curr) {
|
|
|
17380
17436
|
}
|
|
17381
17437
|
function envDetectCmd(root, opts = {}) {
|
|
17382
17438
|
root = absRoot(root || process.cwd());
|
|
17439
|
+
// 1.36.21 (전수 sweep P2/high): handoff 와 동일 갭 — 없는 경로에 .harness/environment.json 을 만들며 exit 0. UR-0136 관례로 정렬.
|
|
17440
|
+
if (!exists(root) || !fs.statSync(root).isDirectory()) { failJson(has('--json') || opts.json, 'path_not_found', `경로 없음 또는 디렉토리 아님: ${root}`); return; }
|
|
17383
17441
|
const jsonMode = has('--json') || opts.json;
|
|
17384
17442
|
const writeMode = !has('--no-write'); // default: 캡처 후 .harness/environment.json 에 저장
|
|
17385
17443
|
const snap = _detectEnvironment(root);
|
|
@@ -20304,37 +20362,39 @@ function reuseAutodetectCmd(root) {
|
|
|
20304
20362
|
}
|
|
20305
20363
|
}
|
|
20306
20364
|
}
|
|
20307
|
-
|
|
20308
|
-
|
|
20309
|
-
|
|
20310
|
-
|
|
20311
|
-
log(`# leerness reuse autodetect (1.9.35)`);
|
|
20312
|
-
log(`project: ${path.basename(root)}`);
|
|
20313
|
-
log(`발견된 capability 후보: ${found.length}건`);
|
|
20314
|
-
log('');
|
|
20315
|
-
log('| Capability | Where | Kind | Note |');
|
|
20316
|
-
log('|---|---|---|---|');
|
|
20317
|
-
for (const c of found) log(`| ${c.name} | ${c.file} | util | (autodetect from module.exports) |`);
|
|
20318
|
-
log('');
|
|
20365
|
+
// 1.36.20 (codex fresh-QA #7): --apply mutation 을 출력(human/json) "전"에 수행. 종전엔 --json 분기가 --apply 앞에서
|
|
20366
|
+
// return 해 `reuse autodetect --apply --json` 이 후보만 보고하고 실제 reuse-map 을 안 쓰던 silent no-op. 이제 먼저 mutate →
|
|
20367
|
+
// applied/added 를 human/json 양쪽에 반영. 에러도 --json 시 구조화(failJson).
|
|
20368
|
+
let applied = false, added = 0;
|
|
20319
20369
|
if (has('--apply')) {
|
|
20320
|
-
// reuse-map.md에 추가 (헤더 보존 + 후보 라인 append)
|
|
20321
20370
|
const reusePath = path.join(root, '.harness', 'reuse-map.md');
|
|
20322
20371
|
if (!exists(reusePath)) {
|
|
20323
|
-
|
|
20324
|
-
return process.exit(1);
|
|
20372
|
+
failJson(has('--json'), 'no_reuse_map', `.harness/reuse-map.md 없음 — leerness init 먼저 실행`);
|
|
20373
|
+
return process.exit(process.exitCode || 1);
|
|
20325
20374
|
}
|
|
20326
20375
|
let body = read(reusePath);
|
|
20327
|
-
let added = 0;
|
|
20328
20376
|
for (const c of found) {
|
|
20329
20377
|
if (body.includes(`| ${c.name} |`)) continue; // 이미 있음
|
|
20330
20378
|
body += `| ${c.name} | ${c.file} | util | autodetect 1.9.35 |\n`;
|
|
20331
20379
|
added++;
|
|
20332
20380
|
}
|
|
20333
20381
|
writeUtf8(reusePath, body);
|
|
20334
|
-
|
|
20335
|
-
} else {
|
|
20336
|
-
log(`(--apply 로 reuse-map.md에 자동 추가)`);
|
|
20382
|
+
applied = true;
|
|
20337
20383
|
}
|
|
20384
|
+
if (has('--json')) {
|
|
20385
|
+
log(JSON.stringify({ project: path.basename(root), found, applied, added }, null, 2));
|
|
20386
|
+
return;
|
|
20387
|
+
}
|
|
20388
|
+
log(`# leerness reuse autodetect (1.9.35)`);
|
|
20389
|
+
log(`project: ${path.basename(root)}`);
|
|
20390
|
+
log(`발견된 capability 후보: ${found.length}건`);
|
|
20391
|
+
log('');
|
|
20392
|
+
log('| Capability | Where | Kind | Note |');
|
|
20393
|
+
log('|---|---|---|---|');
|
|
20394
|
+
for (const c of found) log(`| ${c.name} | ${c.file} | util | (autodetect from module.exports) |`);
|
|
20395
|
+
log('');
|
|
20396
|
+
if (applied) log(`✓ ${added}건 reuse-map.md에 추가됨`);
|
|
20397
|
+
else log(`(--apply 로 reuse-map.md에 자동 추가)`);
|
|
20338
20398
|
}
|
|
20339
20399
|
|
|
20340
20400
|
// 1.9.165: leerness web — playwright bridge MVP (opt-in 의존성, 5능력 #1 보강)
|
|
@@ -21015,7 +21075,17 @@ async function main() {
|
|
|
21015
21075
|
if (cmd === 'migrate' && args[1] === 'apply') return migrateApplyCmd(arg('--path', args[2] && !args[2].startsWith('-') ? args[2] : process.cwd()), { json: has('--json'), yes: has('--yes') }); // 1.9.357 (UR-0075 Phase C): canonical 백필 비파괴 적용 (기본 dry-run)
|
|
21016
21076
|
if (cmd === 'migrate' && args[1] === 'plan') return migratePlanCmd(arg('--path', args[2] && !args[2].startsWith('-') ? args[2] : process.cwd()), { json: has('--json') }); // 1.9.358 (UR-0075 Phase D): 임시폴더 설치 후 비교 마이그레이션 플랜 (읽기 전용)
|
|
21017
21077
|
if (cmd === 'install-safety') return installSafetyCmd({ json: has('--json') }); // 1.9.359 (UR-0074): 설치 안전 프로필 (0 deps / 0 install-script) + 안전 설치 워크플로
|
|
21018
|
-
|
|
21078
|
+
// 1.36.21 (전수 sweep P2): 오타/추측 하위명령(`migrate aduit`, `migrate check`)이 그 토큰을 "경로"로 삼아 새 디렉토리를 만들고
|
|
21079
|
+
// 전체 설치를 수행하던 것 차단(--json 무시 + exit 0 이라 조용했음). migrate 의 전제는 "기존 프로젝트" — 새 트리 생성은 init 의 일.
|
|
21080
|
+
// 존재 기반 가드라 정당 흐름(migrate . / migrate /existing / migrate --path X / migrate)은 전부 보존.
|
|
21081
|
+
if (cmd === 'migrate') {
|
|
21082
|
+
const _msub = args[1];
|
|
21083
|
+
if (_msub && !_msub.startsWith('-') && !has('--path') && !(exists(_msub) && fs.statSync(_msub).isDirectory())) {
|
|
21084
|
+
failJson(has('--json'), 'unknown_subcommand', `알 수 없는 migrate 하위명령/경로: ${_msub} — 사용법: leerness migrate [기존경로] | migrate audit|apply|plan [경로] | migrate --guide (새 프로젝트는 leerness init)`);
|
|
21085
|
+
return;
|
|
21086
|
+
}
|
|
21087
|
+
return await install(arg('--path', _msub || process.cwd()), { force:has('--force'), dry:has('--dry-run'), migration:true }); // 1.9.355 (UR-0075): --path 지원
|
|
21088
|
+
}
|
|
21019
21089
|
if (cmd === 'update') return await updateCmd(arg('--path', args[1] || process.cwd()), { checkOnly: has('--check'), yes: has('--yes'), force: has('--force') }); // 1.9.355 (UR-0075): --path 지원
|
|
21020
21090
|
if (cmd === 'auto-update' && args[1] === 'install') return autoUpdateInstall(arg('--path', args[2] || process.cwd()));
|
|
21021
21091
|
if (cmd === 'status') return status(arg('--path', args[1] || process.cwd()));
|