leerness 1.36.33 → 1.36.35
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 +15 -0
- package/README.md +4 -4
- package/bin/leerness.js +90 -38
- 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,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.35 — 2026-07-15 — deps --run-tests 정직화 + #7 의도적 DEFER (codex 3차 잔여 소진)
|
|
4
|
+
|
|
5
|
+
- **#6 `deps <cap> --run-tests`**: (a) `--json` 이 테스트 스윕 **전에** return → 스윕이 무언 무시되고 exit 0(false-green) — json/human 이 같은 `_runImpactSweep` 를 쓰도록 공용화, json 은 `tests[]` 필드 + 실패 시 exit 1. (b) 프로젝트를 basename 으로 역추적해 **동명 프로젝트(a/service, b/service) 중 하나만 테스트**되고 실패쪽이 조용히 생략 — 절대경로 키로 교정. 실측: 동명 2 프로젝트 모두 실행(PASS/FAIL 각각), json/human 동등, 실패 exit 1.
|
|
6
|
+
- **#7 (주석 트레이스 오인) 의도적 DEFER — 정직 기록**: 주석 속 `fetch('/users')` 가 낙관검사 트레이스로 인정되는 건 사실이나, 교정에는 JS 주석 마스킹이 필요하다. 이 마스커 계열은 1.36.18 에서 7회 반증 끝에 리버트한 전력이 있고(0-deps 제약 하에 파서 없이 안전 불가), 잘못 벗기면 verify-claim `--strict-claims` 가 **false-BLOCK**(허위 실패로 머지 차단) 하는 방향이라 게이트 편향 원칙(false-PASS 편향)에 반한다. 현 한계는 문서화된 known-limitation 으로 유지: 낙관검사는 "주장 vs 텍스트 흔적" 수준의 advisory 신호다.
|
|
7
|
+
- **검증**: selftest 305/305(스윕 공용+절대경로+exit 배선; 자기참조 트랩 4번째 재발 → split-literal), 동명 충돌 원 재현 before/after, exit 전파형 게이트 e2e, 클린룸. **codex 3차 헌트 10건 전건 처분 완료(수정 9 + 정직 DEFER 1).**
|
|
8
|
+
|
|
9
|
+
## 1.36.34 — 2026-07-15 — 판정 정직화 5종 — 도구가 내리는 판정(허용/만료/최신/집계)이 거짓말하지 않도록 (codex 3차 #4/#5/#8/#9/#10)
|
|
10
|
+
|
|
11
|
+
- **#8 policy check**: 차단 판정인데 exit 0 → **exit 1**(CI/스크립트가 판정 신뢰 가능). 손상 policy.json 을 기본값 폴백으로 오답하던 것 → `policy_corrupt` 구조화 에러.
|
|
12
|
+
- **#9 creds check**: null/비객체 레코드 → raw TypeError 크래시, 판독불가 날짜(NaN) → false-ready. → `invalid_credentials_schema` 구조화 판정 exit 1, 판독불가 날짜는 유효로 치지 않음(`unparsable_lastRefreshed`).
|
|
13
|
+
- **#10 session close**: `completed`/`cancelled` 등 별칭·미지 상태가 어느 카테고리에도 안 담겨 마감 보고에서 조용히 소실(3건 중 1건 집계 실측; codex 실측 실프로젝트 13건 누락). → 별칭 정규화(completed/verified→done, cancelled→dropped 등) + 미지 상태 `other` 버킷(집계·handoff 모두 소실 0).
|
|
14
|
+
- **#5 update --check**: 조회 실패(null)를 24h 캐시로 굳혀 다음 실행이 "up to date" 오답 → null 미캐시(다음 실행 재조회, "판정 보류" 명시). `compareVer` prerelease 세그먼트 비교 구현(semver §11 — 종전 beta.10==beta.2) — 숫자/문자/길이 규칙 7케이스 실증, 일반 비교 무회귀.
|
|
15
|
+
- **#4 update --from <tarball>**: Windows shell 이 공백 경로를 재분할해 실패 → 인자 quoting. 이력이 실행 CLI 버전을 기록하던 것 → 설치된 HARNESS_VERSION 재판독.
|
|
16
|
+
- **검증**: selftest 304/304(semver 5케이스 행위 + 배선 5종), 5건 원 재현 before/after, exit 전파형 게이트 e2e, 게시본 클린룸. 이연: #6 deps --run-tests false-green · #7 주석 트레이스 오인(휴리스틱 계열 — 별도 라운드에서 신중 처리).
|
|
17
|
+
|
|
3
18
|
## 1.36.33 — 2026-07-15 — 데이터 보존 5종 — migrate --force 상태 보존 · positional path 클래스 소진 · symlink 미추종 · skill 충돌 가드 · settings 손상 중단 (codex 3차 헌트 + 자체 발견)
|
|
4
19
|
|
|
5
20
|
- **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 경로까지 확장 완결.**
|
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.35 하네스를 사용합니다. 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.35는 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.33는 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.35)** · 매 라운드 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.35: 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.35';
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -3150,6 +3150,34 @@ function _selfTestCases() {
|
|
|
3150
3150
|
const copySafe = /function copyRecursiveSafe[\s\S]{0,400}lstatSync/.test(s);
|
|
3151
3151
|
return forceGuard && skillGuard && settingsGuard && symlinkGuard && copySafe;
|
|
3152
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
|
+
} },
|
|
3171
|
+
{ name: 'deps --run-tests 정직화 (1.36.35, codex 3차 #6): json/human 스윕 공용 + 절대경로 키(동명 프로젝트 오식별 방지) + 실패 시 exit1 (소스가드)', run: () => {
|
|
3172
|
+
const s = read(__filename);
|
|
3173
|
+
const sweepShared = s.includes('function _runImpactSweep(quietLog)') && s.includes("const tests = has('--run-tests') ? _runImpactSweep(true) : undefined;");
|
|
3174
|
+
const absKey = s.includes('fromProjectPath: p,') && s.includes('const impactedPaths = new Set(directImpact.map(e => e.fromProjectPath)');
|
|
3175
|
+
const jsonExit = s.includes('if (tests && tests.some(t => t.passed === false)) process.exitCode = 1;');
|
|
3176
|
+
const humanShared = s.includes('const results = _runImpactSweep(false);');
|
|
3177
|
+
// 구식 basename 역추적이 사라졌는지 (오식별 코드 부재) — split-literal: 이 줄 자신 매치 방지 (자기참조 트랩 4번째)
|
|
3178
|
+
const oldGone = !s.includes('const projPath = allPaths.find(' + 'p => path.basename(p) === projName);');
|
|
3179
|
+
return sweepShared && absKey && jsonExit && humanShared && oldGone;
|
|
3180
|
+
} },
|
|
3153
3181
|
{ name: 'debug 렌즈 (1.36.27, obra/superpowers systematic-debugging): 자기질문 6문항 ko/en 락스텝 + affects 유효 + route bugfix 힌트 + 파일매핑 미확장 — 행위검사', run: () => {
|
|
3154
3182
|
const d = LENS_CATALOG.debug;
|
|
3155
3183
|
if (!d) return false;
|
|
@@ -12320,7 +12348,7 @@ function depsImpactCmd(root, targetCapability) {
|
|
|
12320
12348
|
for (const e of entries) {
|
|
12321
12349
|
allEntries.push({ project: path.basename(p), projectPath: p, entry: e });
|
|
12322
12350
|
for (const dep of e.dependsOn) {
|
|
12323
|
-
allEdges.push({ fromProject: path.basename(p), fromCap: e.capability, toCap: dep });
|
|
12351
|
+
allEdges.push({ fromProject: path.basename(p), fromProjectPath: p, fromCap: e.capability, toCap: dep }); // 1.36.35 (#6): 절대경로 동반 — basename 충돌 시 프로젝트 오식별 방지
|
|
12324
12352
|
}
|
|
12325
12353
|
}
|
|
12326
12354
|
}
|
|
@@ -12330,6 +12358,7 @@ function depsImpactCmd(root, targetCapability) {
|
|
|
12330
12358
|
const targetLower = target.toLowerCase();
|
|
12331
12359
|
const directImpact = allEdges.filter(e => e.toCap.toLowerCase() === targetLower);
|
|
12332
12360
|
const impactedProjects = new Set(directImpact.map(e => e.fromProject));
|
|
12361
|
+
const impactedPaths = new Set(directImpact.map(e => e.fromProjectPath).filter(Boolean)); // 1.36.35 (#6): 절대경로 기준 집합
|
|
12333
12362
|
|
|
12334
12363
|
// 2단계 전이: 영향받은 capability를 또 의존하는 것들 (2-hop)
|
|
12335
12364
|
const transitiveImpact = [];
|
|
@@ -12338,6 +12367,7 @@ function depsImpactCmd(root, targetCapability) {
|
|
|
12338
12367
|
if (e2.toCap.toLowerCase() === e1.fromCap.toLowerCase()) {
|
|
12339
12368
|
transitiveImpact.push({ via: e1.fromCap, ...e2 });
|
|
12340
12369
|
impactedProjects.add(e2.fromProject);
|
|
12370
|
+
if (e2.fromProjectPath) impactedPaths.add(e2.fromProjectPath);
|
|
12341
12371
|
}
|
|
12342
12372
|
}
|
|
12343
12373
|
}
|
|
@@ -12345,13 +12375,39 @@ function depsImpactCmd(root, targetCapability) {
|
|
|
12345
12375
|
// target capability 자체가 어디 등록됐는지
|
|
12346
12376
|
const definedAt = allEntries.filter(e => e.entry.capability.toLowerCase() === targetLower);
|
|
12347
12377
|
|
|
12378
|
+
// 1.36.35 (#6): 테스트 스윕 — json/human 공용 (종전엔 json 이 스윕 전에 return 해 --run-tests 가 무언 무시됐고,
|
|
12379
|
+
// basename 으로 경로를 역추적해 동명 프로젝트 중 하나만 테스트됐다). 절대경로 순회로 둘 다 교정.
|
|
12380
|
+
function _runImpactSweep(quietLog) {
|
|
12381
|
+
const results = [];
|
|
12382
|
+
for (const projPath of impactedPaths) {
|
|
12383
|
+
const projName = path.basename(projPath);
|
|
12384
|
+
const pkgPath = path.join(projPath, 'package.json');
|
|
12385
|
+
if (!exists(pkgPath)) { if (!quietLog) log(` ⚠ ${projName}: package.json 없음 — skip`); results.push({ project: projName, projectPath: projPath, skipped: 'no_package_json' }); continue; }
|
|
12386
|
+
let pkg = null;
|
|
12387
|
+
try { pkg = JSON.parse(read(pkgPath)); } catch {}
|
|
12388
|
+
if (!pkg?.scripts?.test) { if (!quietLog) log(` ⚠ ${projName}: scripts.test 없음 — skip`); results.push({ project: projName, projectPath: projPath, skipped: 'no_test_script' }); continue; }
|
|
12389
|
+
const t0 = Date.now();
|
|
12390
|
+
const r = runCommandSafe('npm test', [], { cwd: projPath, root: projPath, encoding: 'utf8', allowShell: true, scrubSecrets: true, timeout: 5 * 60 * 1000, kind: 'reuse_impact_test' });
|
|
12391
|
+
const elapsed = Date.now() - t0;
|
|
12392
|
+
const out = (r.stdout || '') + (r.stderr || '');
|
|
12393
|
+
const passed = r.status === 0;
|
|
12394
|
+
results.push({ project: projName, projectPath: projPath, passed, status: r.status, elapsedMs: elapsed });
|
|
12395
|
+
if (!quietLog) log(` ${passed ? '✓' : '✗'} ${projName}: ${passed ? 'PASS' : `FAIL (exit ${r.status})`} · ${Math.round(elapsed / 1000)}s`);
|
|
12396
|
+
}
|
|
12397
|
+
return results;
|
|
12398
|
+
}
|
|
12399
|
+
|
|
12348
12400
|
if (has('--json')) {
|
|
12401
|
+
const tests = has('--run-tests') ? _runImpactSweep(true) : undefined;
|
|
12402
|
+
if (tests && tests.some(t => t.passed === false)) process.exitCode = 1;
|
|
12349
12403
|
log(JSON.stringify({
|
|
12350
12404
|
target,
|
|
12351
12405
|
definedAt: definedAt.map(d => ({ project: d.project, element: d.entry.element })),
|
|
12352
12406
|
directImpact,
|
|
12353
12407
|
transitiveImpact,
|
|
12354
|
-
impactedProjects: Array.from(impactedProjects)
|
|
12408
|
+
impactedProjects: Array.from(impactedProjects),
|
|
12409
|
+
impactedProjectPaths: Array.from(impactedPaths),
|
|
12410
|
+
...(tests ? { tests } : {})
|
|
12355
12411
|
}, null, 2));
|
|
12356
12412
|
return;
|
|
12357
12413
|
}
|
|
@@ -12384,32 +12440,12 @@ function depsImpactCmd(root, targetCapability) {
|
|
|
12384
12440
|
if (has('--run-tests')) {
|
|
12385
12441
|
log('');
|
|
12386
12442
|
log(`## 🚦 자동 회귀 sweep (--run-tests)`);
|
|
12387
|
-
const results =
|
|
12388
|
-
for (const projName of impactedProjects) {
|
|
12389
|
-
const projPath = allPaths.find(p => path.basename(p) === projName);
|
|
12390
|
-
if (!projPath) continue;
|
|
12391
|
-
const pkgPath = path.join(projPath, 'package.json');
|
|
12392
|
-
if (!exists(pkgPath)) { log(` ⚠ ${projName}: package.json 없음 — skip`); continue; }
|
|
12393
|
-
let pkg = null;
|
|
12394
|
-
try { pkg = JSON.parse(read(pkgPath)); } catch {}
|
|
12395
|
-
if (!pkg?.scripts?.test) { log(` ⚠ ${projName}: scripts.test 없음 — skip`); continue; }
|
|
12396
|
-
const t0 = Date.now();
|
|
12397
|
-
// 1.9.299 (UR-0039): 영향 프로젝트 npm test → runCommandSafe + scrubSecrets (시크릿 노출 차단). root=projPath 라 cwd jail 통과.
|
|
12398
|
-
const r = runCommandSafe('npm test', [], { cwd: projPath, root: projPath, encoding: 'utf8', allowShell: true, scrubSecrets: true, timeout: 5 * 60 * 1000, kind: 'reuse_impact_test' });
|
|
12399
|
-
const elapsed = Date.now() - t0;
|
|
12400
|
-
const out = (r.stdout || '') + (r.stderr || '');
|
|
12401
|
-
const m = out.match(/(\d+)\s*\/\s*(\d+)\s*(?:passed|통과|pass|passing)/i);
|
|
12402
|
-
const passed = r.status === 0;
|
|
12403
|
-
results.push({ project: projName, passed, exit: r.status, elapsed, parsed: m ? { num: parseInt(m[1], 10), denom: parseInt(m[2], 10) } : null });
|
|
12404
|
-
const tag = passed ? '✓' : '✗';
|
|
12405
|
-
const ratio = m ? ` (${m[1]}/${m[2]})` : '';
|
|
12406
|
-
log(` ${tag} ${projName}: exit=${r.status}${ratio} ${elapsed}ms`);
|
|
12407
|
-
}
|
|
12443
|
+
const results = _runImpactSweep(false); // 1.36.35 (#6): json 과 동일한 절대경로 스윕 공용
|
|
12408
12444
|
log('');
|
|
12409
12445
|
const pass = results.filter(r => r.passed).length;
|
|
12410
|
-
const fail = results.
|
|
12446
|
+
const fail = results.filter(r => r.passed === false).length;
|
|
12411
12447
|
log(`## 종합`);
|
|
12412
|
-
log(` - 영향받는 프로젝트 ${
|
|
12448
|
+
log(` - 영향받는 프로젝트 ${impactedPaths.size}개 중 ${pass}개 통과, ${fail}개 실패`);
|
|
12413
12449
|
if (fail > 0) {
|
|
12414
12450
|
log(` ⚠ ${target} 변경이 ${fail}개 프로젝트에 회귀 발생 가능 — 해당 프로젝트 testing 우선`);
|
|
12415
12451
|
return process.exit(1);
|
|
@@ -16363,16 +16399,17 @@ async function updateCmd(root, opts = {}) {
|
|
|
16363
16399
|
if (fromTar) { if (!quiet) log(`Local tarball mode: ${fromTar}`); }
|
|
16364
16400
|
else {
|
|
16365
16401
|
const cached = readUpdateCache(root);
|
|
16366
|
-
|
|
16367
|
-
|
|
16402
|
+
// 1.36.34 (codex 3차 #5): 조회 실패(null)를 24h 캐시로 굳히면 다음 실행이 "up to date" 로 오답 — null 은 캐시하지 않고 재시도.
|
|
16403
|
+
if (cacheFresh(cached, cacheHours) && cached.nextLeerness) {
|
|
16404
|
+
nextLeerness = cached.nextLeerness;
|
|
16368
16405
|
if (!quiet) log(`(cached ${Math.round((Date.now() - cached.at) / 60000)}m ago)`);
|
|
16369
|
-
if (!quiet) log(`npm leerness latest: ${cached.nextLeerness
|
|
16406
|
+
if (!quiet) log(`npm leerness latest: ${cached.nextLeerness}`);
|
|
16370
16407
|
} else {
|
|
16371
16408
|
if (!quiet) log('Checking npm registry…');
|
|
16372
16409
|
const latest = await fetchNpmLatest('leerness');
|
|
16373
16410
|
nextLeerness = latest || VERSION;
|
|
16374
|
-
writeUpdateCache(root, { nextLeerness: latest, runningCli: VERSION });
|
|
16375
|
-
if (!quiet) log(`npm leerness latest: ${latest || '(unavailable
|
|
16411
|
+
if (latest) writeUpdateCache(root, { nextLeerness: latest, runningCli: VERSION });
|
|
16412
|
+
if (!quiet) log(`npm leerness latest: ${latest || '(unavailable — 판정 보류, 다음 실행에서 재조회)'}`);
|
|
16376
16413
|
}
|
|
16377
16414
|
}
|
|
16378
16415
|
// What is "current"? canonical=base; legacy plus also rolls into leerness 1.9.0+
|
|
@@ -16403,8 +16440,12 @@ async function updateCmd(root, opts = {}) {
|
|
|
16403
16440
|
if (r.status !== 0) { fail(`delegated migrate exited ${r.status}`); process.exitCode = 1; return; }
|
|
16404
16441
|
} else if (fromTar) {
|
|
16405
16442
|
log(`\nDelegating to npx -p ${fromTar} leerness migrate (local tarball)…`);
|
|
16406
|
-
|
|
16443
|
+
// 1.36.34 (codex 3차 #4): Windows shell:true 는 인자를 공백에서 재분할 — 공백 포함 경로를 따옴표로 보호.
|
|
16444
|
+
const _qw = a => (process.platform === 'win32' && /\s/.test(String(a))) ? `"${a}"` : a;
|
|
16445
|
+
const r = cp.spawnSync('npx', ['-y', '-p', _qw(fromTar), 'leerness', 'migrate', _qw(root), '--yes'], { stdio: 'inherit', shell: process.platform === 'win32' });
|
|
16407
16446
|
if (r.status !== 0) { fail(`delegated migrate exited ${r.status}`); process.exitCode = 1; return; }
|
|
16447
|
+
// 1.36.34 (#4b): 이력에 실행 CLI 버전이 아니라 실제 설치된 버전 기록 — 마이그레이션 후 HARNESS_VERSION 재판독.
|
|
16448
|
+
try { const _hv = read(path.join(root, '.harness', 'HARNESS_VERSION')).trim(); if (_hv) nextLeerness = _hv; } catch {}
|
|
16408
16449
|
} else {
|
|
16409
16450
|
log(`\nRunning in-process migrate (already on latest ${VERSION})…`);
|
|
16410
16451
|
await install(root, { force: false, dry: false, migration: true, nonInteractive: true });
|
|
@@ -18788,7 +18829,11 @@ function policyCmd(root, sub, ...args) {
|
|
|
18788
18829
|
if (sub === 'check') {
|
|
18789
18830
|
const cmd = (args.find(a => a && !a.startsWith('-')) || arg('--cmd', '')).trim();
|
|
18790
18831
|
if (!cmd) return fail('policy check "<command>" 필요');
|
|
18832
|
+
// 1.36.34 (codex 3차 #8): 손상 policy.json 을 기본값으로 조용히 폴백하면 잘못된 허용 판정 — 손상은 구조화 에러 + exit 1.
|
|
18833
|
+
const _pf = _policyFile(root);
|
|
18834
|
+
if (exists(_pf)) { try { JSON.parse(read(_pf)); } catch { failJson(json, 'policy_corrupt', `policy 파일 손상(JSON 파싱 실패): ${_pf} — 복구/삭제 후 재시도`); return; } }
|
|
18791
18835
|
const res = _policyEnforce(root, cmd);
|
|
18836
|
+
if (!res.allowed) process.exitCode = 1; // 1.36.34 (#8): 차단 판정은 exit 1 (CI/스크립트가 판정을 신뢰할 수 있게)
|
|
18792
18837
|
if (json) { log(JSON.stringify({ cmd, ...res }, null, 2)); return; }
|
|
18793
18838
|
log(`# policy check: ${cmd}`);
|
|
18794
18839
|
log(` 요구 등급: ${res.required} · 허용 등급: ${res.allowedTier} · ${res.allowed ? '🟢 허용' : '🔴 차단'}${res.advisory ? ' (advisory — enforce OFF)' : ''}`);
|
|
@@ -20351,11 +20396,18 @@ function credsCheckCmd(root, service) {
|
|
|
20351
20396
|
const targets = service ? (j.services[service] ? { [service]: j.services[service] } : {}) : (j.services || {});
|
|
20352
20397
|
if (!Object.keys(targets).length) { failJson(has('--json'), 'no_service', `등록된 서비스 없음${service ? ` (${service})` : ''}`); return; } // 1.9.404 (UR-0105 잔여): --json 에러 구조화
|
|
20353
20398
|
for (const [name, meta] of Object.entries(targets)) {
|
|
20354
|
-
|
|
20355
|
-
|
|
20356
|
-
|
|
20357
|
-
|
|
20358
|
-
|
|
20399
|
+
// 1.36.34 (codex 3차 #9): 손상 레코드(null/비객체) → raw TypeError 크래시, 파싱 불가 날짜(NaN) → false-ready. 구조화 판정.
|
|
20400
|
+
if (!meta || typeof meta !== 'object' || !Array.isArray(meta.envVars)) {
|
|
20401
|
+
result.services[name] = { envSet: false, missing: [], expired: false, invalid: 'invalid_credentials_schema' };
|
|
20402
|
+
result.ok = false;
|
|
20403
|
+
continue;
|
|
20404
|
+
}
|
|
20405
|
+
const missing = meta.envVars.filter(v => !process.env[v]);
|
|
20406
|
+
const _ts = meta.lastRefreshed ? Date.parse(meta.lastRefreshed) : null;
|
|
20407
|
+
const _badDate = meta.lastRefreshed != null && Number.isNaN(_ts);
|
|
20408
|
+
const expired = _badDate ? true // 판독 불가 날짜는 '유효' 로 치지 않음 (false-ready 방지)
|
|
20409
|
+
: (meta.tokenLifetimeHours && _ts != null ? (Date.now() - _ts) > meta.tokenLifetimeHours * 3600 * 1000 : false);
|
|
20410
|
+
result.services[name] = { envSet: !missing.length, missing, expired, ...(_badDate ? { invalid: 'unparsable_lastRefreshed' } : {}) };
|
|
20359
20411
|
if (missing.length || expired) result.ok = false;
|
|
20360
20412
|
}
|
|
20361
20413
|
if (has('--json')) { log(JSON.stringify(result, null, 2)); if (!result.ok) process.exitCode = 1; return; }
|
|
@@ -20363,7 +20415,7 @@ function credsCheckCmd(root, service) {
|
|
|
20363
20415
|
for (const [name, r] of Object.entries(result.services)) {
|
|
20364
20416
|
if (r.envSet && !r.expired) log(` ✓ ${name}: 사용 준비됨`);
|
|
20365
20417
|
else {
|
|
20366
|
-
log(` ⚠ ${name}: ${r.missing.length ? `누락 ${r.missing.join(',')}` : ''}${r.expired ? ' · 토큰 만료 (재로그인 필요)' : ''}`);
|
|
20418
|
+
log(` ⚠ ${name}: ${r.missing.length ? `누락 ${r.missing.join(',')}` : ''}${r.expired ? ' · 토큰 만료 (재로그인 필요)' : ''}${r.invalid ? ` · 레코드 손상(${r.invalid}) — creds register 로 재등록` : ''}`);
|
|
20367
20419
|
}
|
|
20368
20420
|
}
|
|
20369
20421
|
if (!result.ok) process.exitCode = 1;
|
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.35",
|
|
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",
|