leerness 1.36.32 → 1.36.34
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 +100 -26
- package/lib/pure-utils.js +13 -0
- package/lib/session-close.js +11 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.34 — 2026-07-15 — 판정 정직화 5종 — 도구가 내리는 판정(허용/만료/최신/집계)이 거짓말하지 않도록 (codex 3차 #4/#5/#8/#9/#10)
|
|
4
|
+
|
|
5
|
+
- **#8 policy check**: 차단 판정인데 exit 0 → **exit 1**(CI/스크립트가 판정 신뢰 가능). 손상 policy.json 을 기본값 폴백으로 오답하던 것 → `policy_corrupt` 구조화 에러.
|
|
6
|
+
- **#9 creds check**: null/비객체 레코드 → raw TypeError 크래시, 판독불가 날짜(NaN) → false-ready. → `invalid_credentials_schema` 구조화 판정 exit 1, 판독불가 날짜는 유효로 치지 않음(`unparsable_lastRefreshed`).
|
|
7
|
+
- **#10 session close**: `completed`/`cancelled` 등 별칭·미지 상태가 어느 카테고리에도 안 담겨 마감 보고에서 조용히 소실(3건 중 1건 집계 실측; codex 실측 실프로젝트 13건 누락). → 별칭 정규화(completed/verified→done, cancelled→dropped 등) + 미지 상태 `other` 버킷(집계·handoff 모두 소실 0).
|
|
8
|
+
- **#5 update --check**: 조회 실패(null)를 24h 캐시로 굳혀 다음 실행이 "up to date" 오답 → null 미캐시(다음 실행 재조회, "판정 보류" 명시). `compareVer` prerelease 세그먼트 비교 구현(semver §11 — 종전 beta.10==beta.2) — 숫자/문자/길이 규칙 7케이스 실증, 일반 비교 무회귀.
|
|
9
|
+
- **#4 update --from <tarball>**: Windows shell 이 공백 경로를 재분할해 실패 → 인자 quoting. 이력이 실행 CLI 버전을 기록하던 것 → 설치된 HARNESS_VERSION 재판독.
|
|
10
|
+
- **검증**: selftest 304/304(semver 5케이스 행위 + 배선 5종), 5건 원 재현 before/after, exit 전파형 게이트 e2e, 게시본 클린룸. 이연: #6 deps --run-tests false-green · #7 주석 트레이스 오인(휴리스틱 계열 — 별도 라운드에서 신중 처리).
|
|
11
|
+
|
|
12
|
+
## 1.36.33 — 2026-07-15 — 데이터 보존 5종 — migrate --force 상태 보존 · positional path 클래스 소진 · symlink 미추종 · skill 충돌 가드 · settings 손상 중단 (codex 3차 헌트 + 자체 발견)
|
|
13
|
+
|
|
14
|
+
- **P1 — `migrate --force` 가 사용자 상태를 삭제** (codex 3차 #1): --force 가 progress-tracker/plan/task-log/decisions 등 상태 파일까지 새 템플릿으로 덮어써 활성 task 가 archive 에만 남았다(마커 실측 소실). → force 는 가이드/템플릿 문서에만 적용, 상태 파일 14종은 force 여도 보존(`_USER_STATE`). 실측: --force 후 task 잔존 1 · guideline 은 갱신. **1.36.28 의 마이그레이션 유실 전수 검증을 --force 경로까지 확장 완결.**
|
|
15
|
+
- **P2 — positional path 무시 클래스 8종 소진** (자체 발견 — `migrate-workspace-dir <path>` 가 positional 을 무시하고 cwd 를 마이그레이션해 **검증 중 소스 저장소가 실제로 오염**된 사고로 표면화): migrate-workspace-dir/lessons/resume/session-resume/commands/py-check/path-setup/capabilities 에 1.36.21/24 와 동일한 `_taskPositionalPath` 배선. 오염은 사본 제거로 정리(원본 .harness 불변).
|
|
16
|
+
- **P2 — 재귀 복사 symlink 폭주** (1.36.21 이연 #6): 자기참조 junction 이 있으면 copyRec 이 MAX_PATH 에러로 자기종결할 때까지 실복사(63중첩 실측). → `copyRec`/`copyRecursiveSafe` lstat 미추종(+skip 기록). 실측: 중첩 0 · 일반 파일 정상 복사.
|
|
17
|
+
- **P1 — skill install 무경고 덮어쓰기 + raw-name 우회** (codex 3차 #2): 정규화가 `../x` 의 ../ 를 벗겨 traversal 흔적을 지우고 기존 스킬을 조용히 교체. → raw name 의 경로 문자 선검증 + 내용 다른 충돌은 `--force` 요구(교체 전 .bak 백업, 동일 내용 재설치는 멱등).
|
|
18
|
+
- **P2 — auto-update install 이 손상 settings 를 hooks-only 로 교체** (codex 3차 #3, 1.36.28 손상-스토어 클래스): permissions 등 무관 설정 유실. → 파싱 실패 시 원본 보존 + 중단(exit 1). 정상 파일은 종전대로 병합.
|
|
19
|
+
- **검증**: selftest 303/303, 5건 전부 원 재현 before/after + 무회귀(--force 멱등/정상 병합/skill 멱등), exit 전파형 게이트 e2e, 게시본 클린룸.
|
|
20
|
+
- 이연(codex 3차 잔여): #4 tarball 공백경로 · #5 update 캐시 false-fresh + prerelease 비교 · #6 deps --run-tests false-green · #7 주석 트레이스 오인 · #8 policy exit · #9 creds null 스키마 · #10 session close 상태 별칭 — 다음 라운드.
|
|
21
|
+
|
|
3
22
|
## 1.36.32 — 2026-07-15 — e2e 그린 복원 + 게이트 절차 정직 교정 (자기 감사)
|
|
4
23
|
|
|
5
24
|
**정정(정직 기록)**: 1.36.30/1.36.31 의 CHANGELOG 는 "게이트 e2e" 통과를 주장했지만 **사실이 아니었다** — 1.36.30 의 자동생성 전환(roadmap.html→leerness.html)으로 e2e 의 구식 단언 4건이 실패·크래시했는데, 게이트 러너 래퍼(`npm test > log; echo $?`)의 echo 가 exit code 를 0 으로 가려 두 릴리스가 e2e 실패 상태로 게시됐다(28/29 는 진짜 386/386). selftest 와 라이브 클린룸 실증은 두 릴리스 모두 실제로 통과했으므로 게시된 기능 자체는 검증돼 있었으나, e2e 전체 그린 주장은 과잉이었다.
|
package/README.md
CHANGED
|
@@ -121,7 +121,7 @@ MIT
|
|
|
121
121
|
<!-- leerness:project-readme:start -->
|
|
122
122
|
## Leerness Project Harness
|
|
123
123
|
|
|
124
|
-
이 프로젝트는 Leerness v1.36.
|
|
124
|
+
이 프로젝트는 Leerness v1.36.34 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
125
125
|
|
|
126
126
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
127
127
|
|
|
@@ -175,7 +175,7 @@ leerness memory restore decision <date|title>
|
|
|
175
175
|
|
|
176
176
|
### MCP server (외부 AI 통합)
|
|
177
177
|
|
|
178
|
-
Leerness v1.36.
|
|
178
|
+
Leerness v1.36.34는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
|
|
179
179
|
|
|
180
180
|
```jsonc
|
|
181
181
|
// 카테고리별
|
|
@@ -196,7 +196,7 @@ Leerness v1.36.32는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
196
196
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
197
197
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push → 7) session close → 8) 다음 라운드 예약.
|
|
198
198
|
|
|
199
|
-
현재 누적: **70 라운드 (1.9.40 → 1.36.
|
|
199
|
+
현재 누적: **70 라운드 (1.9.40 → 1.36.34)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
200
200
|
|
|
201
201
|
### 성능 가이드
|
|
202
202
|
|
|
@@ -234,6 +234,6 @@ leerness release pack --close --auto-main-push
|
|
|
234
234
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
235
235
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
236
236
|
|
|
237
|
-
Last synced by Leerness v1.36.
|
|
237
|
+
Last synced by Leerness v1.36.34: 2026-07-15
|
|
238
238
|
<!-- leerness:project-readme:end -->
|
|
239
239
|
|
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.34';
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -652,7 +652,8 @@ leerness memory restore <surface> <target> # archive → active 복귀 (DELETE
|
|
|
652
652
|
function copyRecursiveSafe(src, dst) {
|
|
653
653
|
if (!exists(src)) return;
|
|
654
654
|
if (src.includes(path.sep + '.harness' + path.sep + 'archive')) return;
|
|
655
|
-
const st = fs.
|
|
655
|
+
const st = fs.lstatSync(src); // 1.36.33 (#6): symlink 미추종 — 자기참조 링크 시 MAX_PATH 까지 복제 폭주 방지
|
|
656
|
+
if (st.isSymbolicLink()) return;
|
|
656
657
|
if (st.isDirectory()) {
|
|
657
658
|
mkdirp(dst);
|
|
658
659
|
for (const e of fs.readdirSync(src)) {
|
|
@@ -986,16 +987,20 @@ async function install(root, opts = {}) {
|
|
|
986
987
|
};
|
|
987
988
|
drawProgress(0, '');
|
|
988
989
|
let _done = 0;
|
|
990
|
+
// 1.36.33 (codex 3차 #1, P1 데이터 유실): --force 가 사용자 상태 파일(진행 task/plan/결정/로그/인계 등)까지 새 템플릿으로
|
|
991
|
+
// 덮어써 활성 기록이 archive 에만 남았다. force 는 가이드/템플릿 문서에만 적용 — 상태 파일은 force 여도 보존.
|
|
992
|
+
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']);
|
|
989
993
|
for (const [f, c] of Object.entries(files)) {
|
|
990
994
|
const existsNow = exists(path.join(root, f));
|
|
991
995
|
const mergeManaged = managedOverwrite.has(f);
|
|
996
|
+
const effForce = opts.force && !_USER_STATE.has(f);
|
|
992
997
|
if (opts.dry) {
|
|
993
|
-
const action = existsNow ? (mergeManaged ||
|
|
998
|
+
const action = existsNow ? (mergeManaged || effForce ? 'merge/update' : 'preserve') : 'create';
|
|
994
999
|
log(`[dry-run] ${action}: ${f}`);
|
|
995
1000
|
actions.push({ file:f, action });
|
|
996
1001
|
continue;
|
|
997
1002
|
}
|
|
998
|
-
const r = writeIfSafe(root, f, c, { force:
|
|
1003
|
+
const r = writeIfSafe(root, f, c, { force: effForce, mergeManaged, archiveDir: backup.archiveDir });
|
|
999
1004
|
actions.push(r);
|
|
1000
1005
|
_done++;
|
|
1001
1006
|
drawProgress(_done, f);
|
|
@@ -1514,11 +1519,19 @@ async function skillInstallCmd(root, source) {
|
|
|
1514
1519
|
if (!name) { fail('SKILL.md frontmatter에 `name` 필수'); return process.exit(1); }
|
|
1515
1520
|
// .harness/skills/<id>/SKILL.md 저장
|
|
1516
1521
|
// 1.9.350 (UR-0062 외부리뷰): skill id 정규화 — 선행/후행 ./- 제거 + .. 거부 + jail (path traversal 차단)
|
|
1522
|
+
// 1.36.33 (codex 3차 #2): 원문(raw) name 부터 검증 — 정규화가 '../x' 의 ../ 를 벗겨 'x' 로 만들어 traversal 흔적을 지웠다.
|
|
1523
|
+
if (/(^|[\\/])\.\.([\\/]|$)|[\\/]/.test(String(name))) { fail(`유효하지 않은 skill name (path traversal/경로 문자 차단): ${name}`); return process.exit(1); }
|
|
1517
1524
|
const skillId = String(name).toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[.\-]+|[.\-]+$/g, '');
|
|
1518
1525
|
if (!skillId || skillId.includes('..')) { fail(`유효하지 않은 skill id (path traversal 차단): ${name}`); return process.exit(1); }
|
|
1519
1526
|
const _skillsRoot = path.join(root, '.harness', 'skills');
|
|
1520
1527
|
const dir = path.join(_skillsRoot, skillId);
|
|
1521
1528
|
if (!path.resolve(dir).startsWith(path.resolve(_skillsRoot) + path.sep)) { fail(`skill id jail 위반: ${name}`); return process.exit(1); }
|
|
1529
|
+
// 1.36.33 (codex 3차 #2): 같은 id 가 이미 설치돼 있고 내용이 다르면 무경고 덮어쓰기 금지 — --force 로만 교체(교체 전 .bak 백업).
|
|
1530
|
+
const _prevMd = path.join(dir, 'SKILL.md');
|
|
1531
|
+
if (exists(_prevMd) && read(_prevMd) !== body) {
|
|
1532
|
+
if (!has('--force')) { fail(`이미 설치된 skill: ${skillId} — 내용이 다릅니다. 교체하려면 --force (기존본은 SKILL.md.bak 백업)`); return process.exit(1); }
|
|
1533
|
+
try { fs.copyFileSync(_prevMd, _prevMd + '.bak'); } catch {}
|
|
1534
|
+
}
|
|
1522
1535
|
mkdirp(dir);
|
|
1523
1536
|
writeUtf8(path.join(dir, 'SKILL.md'), body);
|
|
1524
1537
|
// skill.json도 함께 (자체 catalog 호환)
|
|
@@ -3118,6 +3131,43 @@ function _selfTestCases() {
|
|
|
3118
3131
|
const pwWired = /_runPreWakeAudit[\s\S]{0,2200}\^\\\|\\s\*T-\\d\{4,\}\\s\*\\\|\\s\*in-progress/.test(s);
|
|
3119
3132
|
return stateLock && teamLock && teamWired && both && pwWired;
|
|
3120
3133
|
} },
|
|
3134
|
+
{ name: 'positional path 잔존 클래스 소진 (1.36.33): migrate-workspace-dir 외 8종 — cwd 오염 차단 (소스가드, 실제 자기 저장소 오염 사고로 발견)', run: () => {
|
|
3135
|
+
const s = read(__filename);
|
|
3136
|
+
const P = "arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd()";
|
|
3137
|
+
const cmds = ["'migrate-workspace-dir'", "'lessons'", "'resume'", "'session-resume'", "'commands'", "'py-check'", "'path-setup' || cmd === 'path'", "'capabilities' || cmd === 'security-surface'"];
|
|
3138
|
+
return cmds.every(c => {
|
|
3139
|
+
const i = s.indexOf('if (cmd === ' + c + ')');
|
|
3140
|
+
if (i < 0) return false;
|
|
3141
|
+
return s.slice(i, s.indexOf('\n', i)).includes(P);
|
|
3142
|
+
});
|
|
3143
|
+
} },
|
|
3144
|
+
{ name: '데이터 보존 3종 (1.36.33, codex 3차 #1/#2/#3): migrate --force 상태파일 보존 + skill 충돌/raw-name 가드 + settings 손상 중단 + symlink 미추종 (소스가드)', run: () => {
|
|
3145
|
+
const s = read(__filename);
|
|
3146
|
+
const forceGuard = s.includes("const effForce = opts.force && !_USER_STATE.has(f);") && s.includes("'.harness/progress-tracker.md', '.harness/plan.md', '.harness/task-log.md'");
|
|
3147
|
+
const skillGuard = s.includes('유효하지 않은 skill name (path traversal/경로 문자 차단)') && s.includes('이미 설치된 skill: ${skillId} — 내용이 다릅니다');
|
|
3148
|
+
const settingsGuard = s.includes('settings.local.json 이 손상돼(JSON 파싱 실패) hook 설치를 중단');
|
|
3149
|
+
const symlinkGuard = s.includes("if (stat.isSymbolicLink()) { report.skippedFiles.push(rel + ' (symlink — 따라가지 않음')") || s.includes('symlink — 따라가지 않음');
|
|
3150
|
+
const copySafe = /function copyRecursiveSafe[\s\S]{0,400}lstatSync/.test(s);
|
|
3151
|
+
return forceGuard && skillGuard && settingsGuard && symlinkGuard && copySafe;
|
|
3152
|
+
} },
|
|
3153
|
+
{ name: '판정 정직화 5종 (1.36.34, codex 3차 #4/#5/#8/#9/#10): prerelease semver + 캐시 null 미고착 + policy exit/손상 + creds 스키마 + close 상태 별칭 — 행위검사', run: () => {
|
|
3154
|
+
const p = require('../lib/pure-utils');
|
|
3155
|
+
// #5: prerelease 세그먼트 비교 (종전 0 반환)
|
|
3156
|
+
const semverOk = p.compareVer('1.0.0-beta.10', '1.0.0-beta.2') === 1 && p.compareVer('1.0.0-alpha', '1.0.0-beta') === -1
|
|
3157
|
+
&& p.compareVer('1.0.0-beta', '1.0.0-beta.1') === -1 && p.compareVer('1.0.0', '1.0.0-beta') === 1 && p.compareVer('1.2.3', '1.2.3') === 0;
|
|
3158
|
+
const s = read(__filename);
|
|
3159
|
+
const cacheOk = s.includes('if (latest) writeUpdateCache(root,') && s.includes("cacheFresh(cached, cacheHours) && cached.nextLeerness");
|
|
3160
|
+
// #8: 차단 exit 1 + 손상 policy 구조화 에러
|
|
3161
|
+
const policyOk = s.includes('if (!res.allowed) process.exitCode = 1;') && s.includes("'policy_corrupt'");
|
|
3162
|
+
// #9: creds 스키마 검증 + 판독불가 날짜
|
|
3163
|
+
const credsOk = s.includes("invalid: 'invalid_credentials_schema'") && s.includes('unparsable_lastRefreshed');
|
|
3164
|
+
// #10: session close 별칭 정규화 + other 버킷 (행위)
|
|
3165
|
+
const sc = read(path.join(path.dirname(__filename), '..', 'lib', 'session-close.js'));
|
|
3166
|
+
const closeOk = sc.includes('_STATUS_ALIAS') && sc.includes("completed: 'done'") && sc.includes("cancelled: 'dropped'") && sc.includes('buckets.other.length');
|
|
3167
|
+
// #4: tarball 공백 경로 quoting + 설치 버전 재판독
|
|
3168
|
+
const tarOk = s.includes('const _qw = a =>') && s.includes("read(path.join(root, '.harness', 'HARNESS_VERSION')).trim(); if (_hv) nextLeerness = _hv;");
|
|
3169
|
+
return semverOk && cacheOk && policyOk && credsOk && closeOk && tarOk;
|
|
3170
|
+
} },
|
|
3121
3171
|
{ name: 'debug 렌즈 (1.36.27, obra/superpowers systematic-debugging): 자기질문 6문항 ko/en 락스텝 + affects 유효 + route bugfix 힌트 + 파일매핑 미확장 — 행위검사', run: () => {
|
|
3122
3172
|
const d = LENS_CATALOG.debug;
|
|
3123
3173
|
if (!d) return false;
|
|
@@ -6165,7 +6215,10 @@ function _migrateWorkspaceDir(root, opts = {}) {
|
|
|
6165
6215
|
const srcAbs = path.join(srcDir, rel);
|
|
6166
6216
|
const dstAbs = path.join(dstDir, rel);
|
|
6167
6217
|
let stat;
|
|
6168
|
-
|
|
6218
|
+
// 1.36.33 (#6): lstat 로 symlink/junction 을 따라가지 않음 — 자기참조 링크(.harness 안 →.harness)가 있으면
|
|
6219
|
+
// 종전엔 MAX_PATH 에러로 자기종결할 때까지 수십 단계를 실제 복사(63중첩 실측)했다. 링크는 skip 기록.
|
|
6220
|
+
try { stat = fs.lstatSync(srcAbs); } catch (e) { report.errors.push(`stat ${rel}: ${e.message}`); return; }
|
|
6221
|
+
if (stat.isSymbolicLink()) { report.skippedFiles.push(rel + ' (symlink — 따라가지 않음)'); return; }
|
|
6169
6222
|
if (stat.isDirectory()) {
|
|
6170
6223
|
if (!dryRun) mkdirp(dstAbs);
|
|
6171
6224
|
let entries;
|
|
@@ -16328,16 +16381,17 @@ async function updateCmd(root, opts = {}) {
|
|
|
16328
16381
|
if (fromTar) { if (!quiet) log(`Local tarball mode: ${fromTar}`); }
|
|
16329
16382
|
else {
|
|
16330
16383
|
const cached = readUpdateCache(root);
|
|
16331
|
-
|
|
16332
|
-
|
|
16384
|
+
// 1.36.34 (codex 3차 #5): 조회 실패(null)를 24h 캐시로 굳히면 다음 실행이 "up to date" 로 오답 — null 은 캐시하지 않고 재시도.
|
|
16385
|
+
if (cacheFresh(cached, cacheHours) && cached.nextLeerness) {
|
|
16386
|
+
nextLeerness = cached.nextLeerness;
|
|
16333
16387
|
if (!quiet) log(`(cached ${Math.round((Date.now() - cached.at) / 60000)}m ago)`);
|
|
16334
|
-
if (!quiet) log(`npm leerness latest: ${cached.nextLeerness
|
|
16388
|
+
if (!quiet) log(`npm leerness latest: ${cached.nextLeerness}`);
|
|
16335
16389
|
} else {
|
|
16336
16390
|
if (!quiet) log('Checking npm registry…');
|
|
16337
16391
|
const latest = await fetchNpmLatest('leerness');
|
|
16338
16392
|
nextLeerness = latest || VERSION;
|
|
16339
|
-
writeUpdateCache(root, { nextLeerness: latest, runningCli: VERSION });
|
|
16340
|
-
if (!quiet) log(`npm leerness latest: ${latest || '(unavailable
|
|
16393
|
+
if (latest) writeUpdateCache(root, { nextLeerness: latest, runningCli: VERSION });
|
|
16394
|
+
if (!quiet) log(`npm leerness latest: ${latest || '(unavailable — 판정 보류, 다음 실행에서 재조회)'}`);
|
|
16341
16395
|
}
|
|
16342
16396
|
}
|
|
16343
16397
|
// What is "current"? canonical=base; legacy plus also rolls into leerness 1.9.0+
|
|
@@ -16368,8 +16422,12 @@ async function updateCmd(root, opts = {}) {
|
|
|
16368
16422
|
if (r.status !== 0) { fail(`delegated migrate exited ${r.status}`); process.exitCode = 1; return; }
|
|
16369
16423
|
} else if (fromTar) {
|
|
16370
16424
|
log(`\nDelegating to npx -p ${fromTar} leerness migrate (local tarball)…`);
|
|
16371
|
-
|
|
16425
|
+
// 1.36.34 (codex 3차 #4): Windows shell:true 는 인자를 공백에서 재분할 — 공백 포함 경로를 따옴표로 보호.
|
|
16426
|
+
const _qw = a => (process.platform === 'win32' && /\s/.test(String(a))) ? `"${a}"` : a;
|
|
16427
|
+
const r = cp.spawnSync('npx', ['-y', '-p', _qw(fromTar), 'leerness', 'migrate', _qw(root), '--yes'], { stdio: 'inherit', shell: process.platform === 'win32' });
|
|
16372
16428
|
if (r.status !== 0) { fail(`delegated migrate exited ${r.status}`); process.exitCode = 1; return; }
|
|
16429
|
+
// 1.36.34 (#4b): 이력에 실행 CLI 버전이 아니라 실제 설치된 버전 기록 — 마이그레이션 후 HARNESS_VERSION 재판독.
|
|
16430
|
+
try { const _hv = read(path.join(root, '.harness', 'HARNESS_VERSION')).trim(); if (_hv) nextLeerness = _hv; } catch {}
|
|
16373
16431
|
} else {
|
|
16374
16432
|
log(`\nRunning in-process migrate (already on latest ${VERSION})…`);
|
|
16375
16433
|
await install(root, { force: false, dry: false, migration: true, nonInteractive: true });
|
|
@@ -16389,7 +16447,12 @@ function autoUpdateInstall(root) {
|
|
|
16389
16447
|
mkdirp(settingsDir);
|
|
16390
16448
|
const settingsFile = path.join(settingsDir, 'settings.local.json');
|
|
16391
16449
|
let settings = {};
|
|
16392
|
-
|
|
16450
|
+
// 1.36.33 (codex 3차 #3, 1.36.28 손상-스토어 클래스): 손상 settings 를 빈 객체로 오인해 hooks-only 로 덮어쓰면
|
|
16451
|
+
// 사용자의 permissions 등 무관 설정이 유실된다. 파싱 실패 시 원본 보존 + 중단.
|
|
16452
|
+
if (exists(settingsFile)) {
|
|
16453
|
+
try { settings = JSON.parse(read(settingsFile)); }
|
|
16454
|
+
catch { fail(`.claude/settings.local.json 이 손상돼(JSON 파싱 실패) hook 설치를 중단합니다 — 파일을 복구한 뒤 재시도: ${settingsFile}`); process.exitCode = 1; return; }
|
|
16455
|
+
}
|
|
16393
16456
|
settings.hooks = settings.hooks || {};
|
|
16394
16457
|
// 1.9.1 P1: legacy 'leerness-plus update' hook 자동 제거 (이전 fork 시절 잔재).
|
|
16395
16458
|
let removedLegacy = 0, upgradedQuiet = 0;
|
|
@@ -18748,7 +18811,11 @@ function policyCmd(root, sub, ...args) {
|
|
|
18748
18811
|
if (sub === 'check') {
|
|
18749
18812
|
const cmd = (args.find(a => a && !a.startsWith('-')) || arg('--cmd', '')).trim();
|
|
18750
18813
|
if (!cmd) return fail('policy check "<command>" 필요');
|
|
18814
|
+
// 1.36.34 (codex 3차 #8): 손상 policy.json 을 기본값으로 조용히 폴백하면 잘못된 허용 판정 — 손상은 구조화 에러 + exit 1.
|
|
18815
|
+
const _pf = _policyFile(root);
|
|
18816
|
+
if (exists(_pf)) { try { JSON.parse(read(_pf)); } catch { failJson(json, 'policy_corrupt', `policy 파일 손상(JSON 파싱 실패): ${_pf} — 복구/삭제 후 재시도`); return; } }
|
|
18751
18817
|
const res = _policyEnforce(root, cmd);
|
|
18818
|
+
if (!res.allowed) process.exitCode = 1; // 1.36.34 (#8): 차단 판정은 exit 1 (CI/스크립트가 판정을 신뢰할 수 있게)
|
|
18752
18819
|
if (json) { log(JSON.stringify({ cmd, ...res }, null, 2)); return; }
|
|
18753
18820
|
log(`# policy check: ${cmd}`);
|
|
18754
18821
|
log(` 요구 등급: ${res.required} · 허용 등급: ${res.allowedTier} · ${res.allowed ? '🟢 허용' : '🔴 차단'}${res.advisory ? ' (advisory — enforce OFF)' : ''}`);
|
|
@@ -20311,11 +20378,18 @@ function credsCheckCmd(root, service) {
|
|
|
20311
20378
|
const targets = service ? (j.services[service] ? { [service]: j.services[service] } : {}) : (j.services || {});
|
|
20312
20379
|
if (!Object.keys(targets).length) { failJson(has('--json'), 'no_service', `등록된 서비스 없음${service ? ` (${service})` : ''}`); return; } // 1.9.404 (UR-0105 잔여): --json 에러 구조화
|
|
20313
20380
|
for (const [name, meta] of Object.entries(targets)) {
|
|
20314
|
-
|
|
20315
|
-
|
|
20316
|
-
|
|
20317
|
-
|
|
20318
|
-
|
|
20381
|
+
// 1.36.34 (codex 3차 #9): 손상 레코드(null/비객체) → raw TypeError 크래시, 파싱 불가 날짜(NaN) → false-ready. 구조화 판정.
|
|
20382
|
+
if (!meta || typeof meta !== 'object' || !Array.isArray(meta.envVars)) {
|
|
20383
|
+
result.services[name] = { envSet: false, missing: [], expired: false, invalid: 'invalid_credentials_schema' };
|
|
20384
|
+
result.ok = false;
|
|
20385
|
+
continue;
|
|
20386
|
+
}
|
|
20387
|
+
const missing = meta.envVars.filter(v => !process.env[v]);
|
|
20388
|
+
const _ts = meta.lastRefreshed ? Date.parse(meta.lastRefreshed) : null;
|
|
20389
|
+
const _badDate = meta.lastRefreshed != null && Number.isNaN(_ts);
|
|
20390
|
+
const expired = _badDate ? true // 판독 불가 날짜는 '유효' 로 치지 않음 (false-ready 방지)
|
|
20391
|
+
: (meta.tokenLifetimeHours && _ts != null ? (Date.now() - _ts) > meta.tokenLifetimeHours * 3600 * 1000 : false);
|
|
20392
|
+
result.services[name] = { envSet: !missing.length, missing, expired, ...(_badDate ? { invalid: 'unparsable_lastRefreshed' } : {}) };
|
|
20319
20393
|
if (missing.length || expired) result.ok = false;
|
|
20320
20394
|
}
|
|
20321
20395
|
if (has('--json')) { log(JSON.stringify(result, null, 2)); if (!result.ok) process.exitCode = 1; return; }
|
|
@@ -20323,7 +20397,7 @@ function credsCheckCmd(root, service) {
|
|
|
20323
20397
|
for (const [name, r] of Object.entries(result.services)) {
|
|
20324
20398
|
if (r.envSet && !r.expired) log(` ✓ ${name}: 사용 준비됨`);
|
|
20325
20399
|
else {
|
|
20326
|
-
log(` ⚠ ${name}: ${r.missing.length ? `누락 ${r.missing.join(',')}` : ''}${r.expired ? ' · 토큰 만료 (재로그인 필요)' : ''}`);
|
|
20400
|
+
log(` ⚠ ${name}: ${r.missing.length ? `누락 ${r.missing.join(',')}` : ''}${r.expired ? ' · 토큰 만료 (재로그인 필요)' : ''}${r.invalid ? ` · 레코드 손상(${r.invalid}) — creds register 로 재등록` : ''}`);
|
|
20327
20401
|
}
|
|
20328
20402
|
}
|
|
20329
20403
|
if (!result.ok) process.exitCode = 1;
|
|
@@ -21603,7 +21677,7 @@ async function main() {
|
|
|
21603
21677
|
if (cmd === 'mcp' && args[1] === 'serve') return mcpServeCmd(absRoot(arg('--path', process.cwd())));
|
|
21604
21678
|
if (cmd === 'gate') return gate(arg('--path', args[1] || process.cwd()));
|
|
21605
21679
|
if (cmd === 'verify-code') return verifyCodeCmd(arg('--path', args[1] || process.cwd()));
|
|
21606
|
-
if (cmd === 'lessons') return lessonsCmd(arg('--path', process.cwd())
|
|
21680
|
+
if (cmd === 'lessons') return lessonsCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd());
|
|
21607
21681
|
if (cmd === 'retro') return retroCmd(arg('--path', args[1] || process.cwd()));
|
|
21608
21682
|
if (cmd === 'insights') return insightsCmd(arg('--path', args[1] || process.cwd()));
|
|
21609
21683
|
if (cmd === 'brainstorm') return brainstormCmd(arg('--path', process.cwd()), args.slice(1).filter(x => !x.startsWith('-')).join(' '));
|
|
@@ -21612,14 +21686,14 @@ async function main() {
|
|
|
21612
21686
|
// 1.9.201: next-action queue CLI (E축 9.5→10)
|
|
21613
21687
|
if (cmd === 'next-action') return nextActionCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
|
|
21614
21688
|
// 1.9.203: leerness resume — auto-resume-plan 읽고 다음 라운드 즉시 안내 (사용자 명시)
|
|
21615
|
-
if (cmd === 'resume') return resumeCmd(arg('--path', process.cwd())
|
|
21689
|
+
if (cmd === 'resume') return resumeCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd());
|
|
21616
21690
|
// 1.9.207: leerness requests <audit|add|list|complete|drop> — 사용자 요청 누락 확인 절차 (사용자 명시)
|
|
21617
21691
|
// 1.36.24 (UR-0027 확장, 실피해 재현): requests 계열만 positional 경로 미지원이라 `requests add "text" C:/other/proj` 가
|
|
21618
21692
|
// 조용히 cwd 프로젝트에 기록하고 성공 보고했다(task/rule 은 _taskPositionalPath 지원 → 불일치가 실수를 유도, 본인 피해 확인).
|
|
21619
21693
|
// rule 선례와 동일 배선 — path-like(선행 구분자) 만 인정하는 기존 계약이라 텍스트/ID 오인 FP 없음(_parseAddTitle 이 add 텍스트를 path-like 에서 끊는 것도 기존과 동일).
|
|
21620
21694
|
if (cmd === 'requests') return requestsCmd(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd(), args[1], ...args.slice(2));
|
|
21621
21695
|
// 1.9.220: leerness session-resume — 비정상 종료 감지 + 자율 재개 가이드 (사용자 명시)
|
|
21622
|
-
if (cmd === 'session-resume') return sessionResumeCmd(arg('--path', process.cwd())
|
|
21696
|
+
if (cmd === 'session-resume') return sessionResumeCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd());
|
|
21623
21697
|
// 1.9.226: leerness round-history — 자율 라운드 통계 + 다음 마일스톤
|
|
21624
21698
|
if (cmd === 'round-history') return roundHistoryCmd(_resolveRoot(args[1]));
|
|
21625
21699
|
// 1.9.229: leerness milestones — 도달 마일스톤 + 다음 ETA (1.9.226 확장)
|
|
@@ -21629,9 +21703,9 @@ async function main() {
|
|
|
21629
21703
|
if (cmd === 'toggle') return _tgl.toggleCmd(arg('--path', process.cwd()), args[1], args[2], args[3], { has, VERSION }); // 1.36.30: 기능 토글 (그래프 ⚙ 탭 연동)
|
|
21630
21704
|
if (cmd === 'lens') return lensCmd(args[1]); // 1.18.3 (UR-0003): 분야별 자기질문 품질 렌즈
|
|
21631
21705
|
// 1.9.233: leerness commands — 카테고리화된 전체 CLI 명령 목록
|
|
21632
|
-
if (cmd === 'commands') return commandsCmd(arg('--path', process.cwd())
|
|
21706
|
+
if (cmd === 'commands') return commandsCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd());
|
|
21633
21707
|
// 1.9.239: leerness py-check — Python 파일 분석 (사용자 명시 UR-0013)
|
|
21634
|
-
if (cmd === 'py-check') return pyCheckCmd(arg('--path', process.cwd())
|
|
21708
|
+
if (cmd === 'py-check') return pyCheckCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd());
|
|
21635
21709
|
// 1.9.239: leerness agent-mode <start|tick|stop> — 자율 모드 전용 통합 명령 (사용자 명시 UR-0013)
|
|
21636
21710
|
if (cmd === 'agent-mode') return agentModeCmd(arg('--path', process.cwd()), args[1]);
|
|
21637
21711
|
// 1.9.241: leerness env summary|encoding — 환경 종합 + 셸 스크립트 인코딩 위험 (사용자 명시 UR-0014)
|
|
@@ -21641,7 +21715,7 @@ async function main() {
|
|
|
21641
21715
|
return envCmd((args[2] && !args[2].startsWith('-')) ? args[2] : arg('--path', process.cwd()), args[1] === 'summary' ? null : 'encoding-check');
|
|
21642
21716
|
// 1.9.254: leerness path-setup [--apply] — CLI PATH 자동 등록 (사용자 명시 UR-0019)
|
|
21643
21717
|
// npm global bin 경로 감지 + leerness 가 PATH 에서 찾아지는지 확인 → 미등록 시 플랫폼별 등록
|
|
21644
|
-
if (cmd === 'path-setup' || cmd === 'path') return pathSetupCmd(arg('--path', process.cwd()
|
|
21718
|
+
if (cmd === 'path-setup' || cmd === 'path') return pathSetupCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd(), { apply: has('--apply'), json: has('--json') });
|
|
21645
21719
|
// 1.9.258: leerness selftest — 설치된 바이너리 코어 함수 무결성 자가 검증 (CI/사용자 진단)
|
|
21646
21720
|
if (cmd === 'selftest' || cmd === 'self-test') return selfTestCmd({ json: has('--json') });
|
|
21647
21721
|
// 1.9.260: leerness shell-guard "<cmd>" — 터미널 명령 셸 호환성 린터 + 실패 메모리 (사용자 명시 UR-0020)
|
|
@@ -21658,7 +21732,7 @@ async function main() {
|
|
|
21658
21732
|
// 1.9.270: leerness roles <list|set|unset|catalog|suggest|verify> — 모델별 역할 부여 (사용자 명시)
|
|
21659
21733
|
if (cmd === 'roles' || cmd === 'role') return rolesCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
|
|
21660
21734
|
// 1.9.272: leerness capabilities — 권한/보안 표면 공개 (GPT-5.5 외부 리뷰 반영)
|
|
21661
|
-
if (cmd === 'capabilities' || cmd === 'security-surface') return capabilitiesCmd(arg('--path', process.cwd()
|
|
21735
|
+
if (cmd === 'capabilities' || cmd === 'security-surface') return capabilitiesCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd(), { json: has('--json') });
|
|
21662
21736
|
// 1.9.278 (UR-0032): leerness state <show|start|record|verify|handoff> — .leerness/ 구조화 상태 substrate
|
|
21663
21737
|
if (cmd === 'state') return _guardStore(has('--json'), () => stateCmd(arg('--path', process.cwd()), args[1], ...args.slice(2))); // 1.36.28 (#3): 손상 스토어 클로버 차단
|
|
21664
21738
|
if (cmd === 'context') return contextCmd(_resolveRoot(args[1]), { json: has('--json') });
|
|
@@ -21700,7 +21774,7 @@ async function main() {
|
|
|
21700
21774
|
if (cmd === 'workspace-dir') return workspaceDirCmd(arg('--path', process.cwd()), args[1]);
|
|
21701
21775
|
if (cmd === 'parent') return parentCmd(arg('--path', process.cwd()), args[1]);
|
|
21702
21776
|
// 1.9.211: leerness migrate-workspace-dir — .harness → .leerness 마이그레이션 (사용자 명시)
|
|
21703
|
-
if (cmd === 'migrate-workspace-dir') return migrateWorkspaceDirCmd(arg('--path', process.cwd()))
|
|
21777
|
+
if (cmd === 'migrate-workspace-dir') return migrateWorkspaceDirCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd()); // 1.36.33: positional path 무시 → cwd 오염 (1.36.21/24 계열 잔존 — 실제로 이 버그로 소스 저장소가 오염될 뻔)
|
|
21704
21778
|
// 1.9.212: leerness idempotency audit — 멱등성 위반 탐지 (사용자 명시)
|
|
21705
21779
|
if (cmd === 'idempotency') return idempotencyCmd(arg('--path', process.cwd()), args[1]);
|
|
21706
21780
|
// 1.9.213: leerness intent <classify|expand|domains> — intent inference + scope expansion (사용자 명시)
|
package/lib/pure-utils.js
CHANGED
|
@@ -23,6 +23,19 @@ function compareVer(a, b) {
|
|
|
23
23
|
const preA = A.includes('-'), preB = B.includes('-');
|
|
24
24
|
if (preA && !preB) return -1;
|
|
25
25
|
if (!preA && preB) return 1;
|
|
26
|
+
// 1.36.34 (codex 3차 #5): 둘 다 pre-release 면 식별자 세그먼트 비교(semver §11) — 종전엔 0 반환이라 beta.10 == beta.2 오답.
|
|
27
|
+
if (preA && preB) {
|
|
28
|
+
const pa = A.slice(A.indexOf('-') + 1).split('.'), pb = B.slice(B.indexOf('-') + 1).split('.');
|
|
29
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
30
|
+
const x = pa[i], y = pb[i];
|
|
31
|
+
if (x === undefined) return -1; // 짧은 쪽이 낮음 (semver: 세그먼트 적은 쪽 <)
|
|
32
|
+
if (y === undefined) return 1;
|
|
33
|
+
const nx = /^\d+$/.test(x), ny = /^\d+$/.test(y);
|
|
34
|
+
if (nx && ny) { const d = parseInt(x, 10) - parseInt(y, 10); if (d) return d > 0 ? 1 : -1; }
|
|
35
|
+
else if (nx !== ny) return nx ? -1 : 1; // 숫자 < 문자 (semver 규칙)
|
|
36
|
+
else if (x !== y) return x > y ? 1 : -1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
26
39
|
return 0;
|
|
27
40
|
}
|
|
28
41
|
|
package/lib/session-close.js
CHANGED
|
@@ -26,10 +26,19 @@ function sessionClose(root, opts = {}, deps = {}) {
|
|
|
26
26
|
const rows = readProgressRows(root);
|
|
27
27
|
const buckets = {};
|
|
28
28
|
for (const s of STATUSES) buckets[s] = [];
|
|
29
|
-
|
|
29
|
+
// 1.36.34 (codex 3차 #10): 별칭/미지 상태가 어느 카테고리에도 안 담겨 마감 보고에서 조용히 사라짐
|
|
30
|
+
// (completed/cancelled 3건 중 1건만 집계 실측). 별칭은 정규화, 미지는 'other' 버킷으로 — 소실 0.
|
|
31
|
+
const _STATUS_ALIAS = { completed: 'done', complete: 'done', finished: 'done', verified: 'done', cancelled: 'dropped', canceled: 'dropped', cancel: 'dropped', in_progress: 'in-progress', wip: 'in-progress', on_hold: 'on-hold', hold: 'on-hold' };
|
|
32
|
+
buckets.other = [];
|
|
33
|
+
for (const r of rows) {
|
|
34
|
+
const s0 = String(r.status || '').toLowerCase().trim();
|
|
35
|
+
const s = buckets[s0] ? s0 : (_STATUS_ALIAS[s0] && buckets[_STATUS_ALIAS[s0]] ? _STATUS_ALIAS[s0] : 'other');
|
|
36
|
+
buckets[s].push(r);
|
|
37
|
+
}
|
|
30
38
|
// 1.9.103: JSON 결과 누적
|
|
31
39
|
jsonResult.taskCounts = {};
|
|
32
40
|
for (const s of STATUSES) jsonResult.taskCounts[s] = (buckets[s] || []).length;
|
|
41
|
+
if (buckets.other.length) jsonResult.taskCounts.other = buckets.other.length;
|
|
33
42
|
jsonResult.recommendedDirection = (buckets['in-progress'][0]?.request) || (buckets['planned'][0]?.request) || (buckets['requested'][0]?.request) || null;
|
|
34
43
|
jsonResult.nextExactStep = (buckets['in-progress'][0]?.nextAction) || (buckets['planned'][0]?.nextAction) || (buckets['requested'][0]?.nextAction) || null;
|
|
35
44
|
// 1.12.3 (14th 버그헌트 P3, UR-0183): 마감 시 완료 정직성 advisory — done 인데 evidence 가 비었거나 placeholder 인 task 노출(차단 X, 정직성 환기). lazy detect 의 done_no_evidence 휴리스틱과 동일.
|
|
@@ -72,7 +81,7 @@ function sessionClose(root, opts = {}, deps = {}) {
|
|
|
72
81
|
rowsToList(buckets['in-progress']),
|
|
73
82
|
``,
|
|
74
83
|
`## Incomplete / Waiting / On Hold / Blocked`,
|
|
75
|
-
rowsToList([...(buckets['incomplete']||[]), ...(buckets['waiting']||[]), ...(buckets['on-hold']||[]), ...(buckets['blocked']||[])]),
|
|
84
|
+
rowsToList([...(buckets['incomplete']||[]), ...(buckets['waiting']||[]), ...(buckets['on-hold']||[]), ...(buckets['blocked']||[]), ...(buckets.other||[])]), // 1.36.34 (#10): 미지 상태도 소실 없이 표기
|
|
76
85
|
``,
|
|
77
86
|
`## Dropped`,
|
|
78
87
|
rowsToList(buckets['dropped']),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.34",
|
|
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",
|