leerness 1.36.40 → 1.36.42
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 +20 -0
- package/README.md +4 -4
- package/bin/leerness.js +137 -39
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.42 — 2026-07-16 — 판정 정직화 배치 C — codex 5차 헌트 10건 전건 처분 완결 (#4~#10)
|
|
4
|
+
|
|
5
|
+
- **#4 agent-mode**: 3개 자식 전부 실패해도 "자율 라운드 진행 준비" 선언 + exit 0 — 전부 실패=exit 1, 부분 실패=⚠ 명시, tick/stop 자식 실패 전파, 미지 하위명령 exit 1.
|
|
6
|
+
- **#5 orchestrate**: `--agents NaN/0/-1` 이 조용히 1로 화하고 전원 실패에도 exit 0 → 정수 1~256 검증(`invalid_agents`) + 성공 0=exit 1.
|
|
7
|
+
- **#6 release bump/note**: "banana" 를 0.0.1 로 덮어쓰기 → semver 검증 후 거부(`invalid_version`, 원본 보존) · prerelease 는 npm 의미론(1.2.3-beta.2→1.2.3 안정화) · 멀티라인 노트가 CHANGELOG 에 가짜 버전 헤더를 만들던 것 → 한 리스트 항목으로 정규화.
|
|
8
|
+
- **#7 consistency merge-design-guide**: `--dry-run` 실기록 + 재실행마다 중복 append → dry-run 무기록 + 소스별 멱등(이미 병합 skip 표시).
|
|
9
|
+
- **#8 whats-new `--to`**: value-flag 미등록으로 값이 positional 경로로 흡수돼 **엉뚱한(패키지 내장) CHANGELOG 를 읽던 것** → 등록. 실측: --to 지정 시에도 로컬 CHANGELOG.
|
|
10
|
+
- **#9 lens 커스텀**: 손상 quality-lenses.json 무언 무시 → 경고 노출(advisory 유지) · `constructor` 도메인명이 프로토타입 상속 키로 오인돼 크래시 → 자기 소유 키 검사.
|
|
11
|
+
- **#10 register-pending `--json`**: 사람용 3줄이 JSON 앞에 섞여 파서 실패 → 단일 JSON 문서. (review/persona/guide 의 JSON 계약은 별도 라운드 — 출력 구조 재설계 필요라 이연 명기.)
|
|
12
|
+
- **검증**: selftest 312/312, 13케이스 일괄 실증 스크립트 전부 ✓, exit 전파형 게이트 e2e, 클린룸. **codex 5차 10건 전건 처분(수정 10, guide-json 만 부분 이연).**
|
|
13
|
+
|
|
14
|
+
## 1.36.41 — 2026-07-16 — 데이터 보존 3종 2차 — .mcp.json 손상 중단 · register-pending 락 · glossary 마커 보존 (codex 5차 헌트 #1/#2/#3 High)
|
|
15
|
+
|
|
16
|
+
5차 헌트(adapter/ci/orchestrate/guide/doctor/glossary/consistency/release/lens 표면)의 High 3건 — 전부 기존 결함 클래스의 새 발화점:
|
|
17
|
+
|
|
18
|
+
- **#1 adapter 가 손상 `.mcp.json` 파괴** (1.36.28 손상-스토어 클래스): 파싱 실패를 `{}` 로 오인해 사용자의 다른 MCP 서버 등록을 통째 교체 + 지침 파일은 검증 전에 써서 부분 적용. → 파싱 실패/비객체 루트는 **선검증 후 아무것도 쓰지 않고 중단**(`store_corrupt` exit 1). 실측: keep 서버 보존·지침 미생성·정상 병합 무회귀.
|
|
19
|
+
- **#2 병렬 `register-pending` 무성 유실** (1.36.31 락 클래스): ID 산출이 락 밖이라 같은 T-ID 배정→상호 덮어쓰기(30중 25 잔존 실측 — 다중 세션 등록이 이 명령의 명시 용도). → ID 산출→행 작성→저장 한 임계구역. 실측: **24병렬 24/24 잔존**.
|
|
20
|
+
- **#3 glossary 재생성이 수동 내용 삭제**: 마커(GLOSSARY_START/END)가 있는데 통파일 덮어쓰기 → 마커 밖 서문/부록 소실. → 생성 블록만 splice. 실측: 서문+부록+생성블록 공존.
|
|
21
|
+
- **검증**: selftest 311/311, 3건 원 재현 before/after + 무회귀, exit 전파형 게이트 e2e, 클린룸. 이연(5차 잔여): #4 agent-mode 허위 준비완료 · #5 orchestrate 검증 · #6 release bump semver · #7 consistency dry-run · #8 whats-new --to · #9 lens 커스텀 · #10 JSON 계약 4종.
|
|
22
|
+
|
|
3
23
|
## 1.36.40 — 2026-07-16 — 사용자-대면 주장 사실성 감사 — 드리프트 수치 소거 (정기 정직성 점검)
|
|
4
24
|
|
|
5
25
|
22 릴리스 동안 README/docs 의 정량 주장이 코드와 어긋났는지 전수 대조(주기 점검 — 과거 1.36.5 감사의 후속).
|
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.42 하네스를 사용합니다. 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.42는 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.40는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
196
196
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
197
197
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) 회귀 테스트 갱신 → 4) 전체 e2e 스위트 통과 → 5) npm publish + git tag → 6) main push → 7) session close → 8) 다음 라운드 예약.
|
|
198
198
|
|
|
199
|
-
현재 누적: **v1.9.x → 1.36.
|
|
199
|
+
현재 누적: **v1.9.x → 1.36.42 릴리스 태그 이력** (수백 라운드) · _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.42: 2026-07-16
|
|
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.42';
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -218,7 +218,7 @@ function _resolveRoot(positional) {
|
|
|
218
218
|
}
|
|
219
219
|
function nonFlagArgs() {
|
|
220
220
|
const out = [];
|
|
221
|
-
const withValue = new Set(['--language','--skills','--path','--status','--progress','--goal','--reason','--next','--target','--token-env','--package','--out','--from','--repo','--id','--note','--evidence','--query','--limit','--action','--agent','--tool','--doc','--command','--capability','--before','--after','--display','--threshold','--trigger','--check','--set','--min-score','--include','--days','--gh-pages-src','--roadmap','--since','--agents','--model','--timeout','--retry-on-fail','--label','--score','--tokens','--alternatives','--impact','--tag','--surface','--depends-on','--affects','--co-changes-with','--files','--branch','--remote','--task-add','--next-action','--role','--provider','--env-var','--deploy','--token-lifetime-hours','--port','--secret','--keep','--shell','--ps-version','--done-when','--test-cmd']); // 1.14.2 (UR-0032): --done-when 값이 positional 로 누출돼 milestone 제목에 흡수되던 것 차단. 1.17.2 (UR-0045): --test-cmd 동일 원칙(신규 value-flag 는 반드시 여기 등록)
|
|
221
|
+
const withValue = new Set(['--language','--skills','--path','--status','--progress','--goal','--reason','--next','--target','--token-env','--package','--out','--from','--to','--repo','--id','--note','--evidence','--query','--limit','--action','--agent','--tool','--doc','--command','--capability','--before','--after','--display','--threshold','--trigger','--check','--set','--min-score','--include','--days','--gh-pages-src','--roadmap','--since','--agents','--model','--timeout','--retry-on-fail','--label','--score','--tokens','--alternatives','--impact','--tag','--surface','--depends-on','--affects','--co-changes-with','--files','--branch','--remote','--task-add','--next-action','--role','--provider','--env-var','--deploy','--token-lifetime-hours','--port','--secret','--keep','--shell','--ps-version','--done-when','--test-cmd']); // 1.14.2 (UR-0032): --done-when 값이 positional 로 누출돼 milestone 제목에 흡수되던 것 차단. 1.17.2 (UR-0045): --test-cmd 동일 원칙(신규 value-flag 는 반드시 여기 등록)
|
|
222
222
|
const a = process.argv.slice(2);
|
|
223
223
|
for (let i = 0; i < a.length; i++) {
|
|
224
224
|
const x = a[i];
|
|
@@ -2396,7 +2396,11 @@ function agentModeCmd(root, sub) {
|
|
|
2396
2396
|
const r3 = spawnChild(['handoff', root]);
|
|
2397
2397
|
if (r3.status === 0) log(gr(` ✓ handoff 출력 완료 (위 참조)`)); else log(yl(` ⚠ handoff 종료 ${r3.status}`));
|
|
2398
2398
|
log('');
|
|
2399
|
-
|
|
2399
|
+
// 1.36.42 (codex 5차 #4): 세 자식 전부 실패(status null 포함)여도 "준비" 를 선언하던 허위 성공 — 실패 수 기준 정직 판정.
|
|
2400
|
+
const _fails = [r1, r2, r3].filter(r => r.status !== 0).length;
|
|
2401
|
+
if (_fails === 3) { fail(`agent-mode start 실패 — 3개 단계 전부 실패 (경로/설치 확인: ${root})`); process.exitCode = 1; return; }
|
|
2402
|
+
if (_fails > 0) { log(yl(`⚠ agent-mode start 부분 완료 — ${_fails}개 단계 실패 (위 ⚠ 참조)`)); }
|
|
2403
|
+
else log(gr(`✓ agent-mode start 완료 — 자율 라운드 진행 준비`));
|
|
2400
2404
|
return;
|
|
2401
2405
|
}
|
|
2402
2406
|
if (sub === 'tick') {
|
|
@@ -2404,6 +2408,7 @@ function agentModeCmd(root, sub) {
|
|
|
2404
2408
|
log(cy(`# leerness agent-mode tick (1.9.239) — 가벼운 상태 확인`));
|
|
2405
2409
|
const r = spawnChild(['pulse', '--path', root]);
|
|
2406
2410
|
process.stdout.write(r.stdout || '');
|
|
2411
|
+
if (r.status !== 0) process.exitCode = 1; // 1.36.42 (#4): 자식 실패 전파
|
|
2407
2412
|
return;
|
|
2408
2413
|
}
|
|
2409
2414
|
if (sub === 'stop') {
|
|
@@ -2411,10 +2416,12 @@ function agentModeCmd(root, sub) {
|
|
|
2411
2416
|
log(dm(` → session close --auto-apply-delivered --auto-cleanup-branches --auto-fix-encoding`));
|
|
2412
2417
|
const r = spawnChild(['session', 'close', root, '--auto-apply-delivered', '--auto-cleanup-branches', '--auto-fix-encoding']);
|
|
2413
2418
|
process.stdout.write(r.stdout || '');
|
|
2419
|
+
if (r.status !== 0) process.exitCode = 1; // 1.36.42 (#4): 자식 실패 전파
|
|
2414
2420
|
return;
|
|
2415
2421
|
}
|
|
2416
|
-
|
|
2422
|
+
fail(`unknown subcommand: ${sub}`); // 1.36.42 (#4): 미지 하위명령 exit 1 (종전 exit 0)
|
|
2417
2423
|
log(dm(` → leerness agent-mode help`));
|
|
2424
|
+
process.exitCode = 1;
|
|
2418
2425
|
}
|
|
2419
2426
|
|
|
2420
2427
|
// 1.9.234: recentChanges — 최근 N 라운드의 핵심 변경 요약 (git tag commit subject)
|
|
@@ -3273,6 +3280,40 @@ function _selfTestCases() {
|
|
|
3273
3280
|
const pulseOk = s.includes("notComputed: ['security', 'health', 'driftScore']");
|
|
3274
3281
|
return mcpOk && declOk && benchRecOk && benchOk && pulseOk;
|
|
3275
3282
|
} },
|
|
3283
|
+
{ name: '데이터 보존 3종-2차 (1.36.41, codex 5차 #1/#2/#3 High): .mcp.json 손상 중단 + register-pending 락 + glossary 마커 splice — 행위검사', run: () => {
|
|
3284
|
+
const s = read(__filename);
|
|
3285
|
+
// #1: 손상 .mcp.json throw + adapter 선검증 + dispatch 가드
|
|
3286
|
+
const mcpGuard = s.includes('.mcp.json 이 손상돼(JSON 파싱 실패) 병합을 중단') && s.includes("if (a.mcp) _assertStoreParsable(path.join(root, '.mcp.json')") && s.includes('_guardStore(has(\'--json\'), () => adapterCmd(');
|
|
3287
|
+
// #2: register-pending ID 산출~저장 임계구역
|
|
3288
|
+
const rpLock = /registerPendingCmd[\s\S]{0,900}_withLock\(progressPath\(root\), \(\) => \{/.test(s);
|
|
3289
|
+
// #3: glossary 마커 splice (행위)
|
|
3290
|
+
let gOk = false;
|
|
3291
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_gl_'));
|
|
3292
|
+
try {
|
|
3293
|
+
fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true });
|
|
3294
|
+
const p = require('../lib/pure-utils');
|
|
3295
|
+
fs.writeFileSync(path.join(tmp, '.harness', 'glossary.md'), 'PREFACE\n' + p.GLOSSARY_START + '\nold\n' + p.GLOSSARY_END + '\nAPPENDIX\n');
|
|
3296
|
+
// splice 로직은 glossaryCmd 내부 — 소스가드 + 위 실측(E2E 사슬)으로 검증. 여기선 마커 상수 존재만 재확인.
|
|
3297
|
+
gOk = s.includes('_prev.slice(0, _si) + _gen + _prev.slice(_ei + GLOSSARY_END.length)');
|
|
3298
|
+
} catch {} finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
3299
|
+
return mcpGuard && rpLock && gOk;
|
|
3300
|
+
} },
|
|
3301
|
+
{ name: '판정 정직화 배치C (1.36.42, codex 5차 #4~#10): agent-mode/orchestrate exit + release semver/note + consistency dry-run·멱등 + whats-new --to + lens 커스텀 + JSON 순수 (소스가드+행위)', run: () => {
|
|
3302
|
+
const s = read(__filename);
|
|
3303
|
+
const amOk = s.includes('if (_fails === 3) { fail(`agent-mode start 실패') && s.includes('미지 하위명령 exit 1');
|
|
3304
|
+
const orOk = s.includes("'invalid_agents'") && s.includes('if (!ok.length) process.exitCode = 1;');
|
|
3305
|
+
const relOk = s.includes("'invalid_version'") && s.includes('멀티라인은 한 리스트 항목의 연속 들여쓰기로 정규화');
|
|
3306
|
+
const toOk = s.includes("'--from','--to','--repo'");
|
|
3307
|
+
const conOk = s.includes('이미 병합됨(skip)') && /mergeDesign[\s\S]{0,600}has\('--dry-run'\)/.test(s);
|
|
3308
|
+
const lensOk = s.includes('function _loadProjectLensesEx') && s.includes('Object.prototype.hasOwnProperty.call(out, k)');
|
|
3309
|
+
const rpJsonOk = /registerPendingCmd[\s\S]{0,1600}if \(has\('--json'\)\) \{ log\(JSON\.stringify\(\{ ok: true, id, \.\.\.row \}, null, 2\)\); return; \}/.test(s);
|
|
3310
|
+
// 행위: prerelease 안정화 + 프로토타입 키 무크래시
|
|
3311
|
+
const p = require('../lib/pure-utils');
|
|
3312
|
+
const preOk = p.compareVer('1.2.3', '1.2.3-beta.2') === 1; // bump 결과가 상위인지 간접 확인
|
|
3313
|
+
let protoOk = false;
|
|
3314
|
+
try { const m = _mergeLensCatalog(LENS_CATALOG, { constructor: { title: 'C', persona: 'r', questions: ['q1'] } }); protoOk = !!(m.constructor && Array.isArray(m.constructor.questions) && m.constructor.questions[0] === 'q1'); } catch {}
|
|
3315
|
+
return amOk && orOk && relOk && toOk && conOk && lensOk && rpJsonOk && preOk && protoOk;
|
|
3316
|
+
} },
|
|
3276
3317
|
{ name: 'debug 렌즈 (1.36.27, obra/superpowers systematic-debugging): 자기질문 6문항 ko/en 락스텝 + affects 유효 + route bugfix 힌트 + 파일매핑 미확장 — 행위검사', run: () => {
|
|
3277
3318
|
const d = LENS_CATALOG.debug;
|
|
3278
3319
|
if (!d) return false;
|
|
@@ -5561,12 +5602,18 @@ const LENS_CATALOG = {
|
|
|
5561
5602
|
};
|
|
5562
5603
|
// 1.19.3 (UR-0003 렌즈 완전판 v3): 프로젝트별 커스텀 렌즈 — .harness/quality-lenses.json 읽기-병합(쓰기 명령 없음, AI/사용자가 편집).
|
|
5563
5604
|
// 포맷: { "domains": { "code": { "questions": ["추가 질문"] }, "a11y": { "title":"접근성", "persona":"스크린리더 사용자", "questions":[...], "affects":["design"] } } }
|
|
5564
|
-
|
|
5605
|
+
// 1.36.42 (codex 5차 #9): 손상 파일을 무언 무시하면 사용자는 커스텀 렌즈가 적용된 줄 안다 — 에러를 함께 반환(Ex), 기존 시그니처는 래퍼.
|
|
5606
|
+
function _loadProjectLensesEx(root) {
|
|
5607
|
+
const p = path.join(absRoot(root || process.cwd()), '.harness', 'quality-lenses.json');
|
|
5608
|
+
if (!exists(p)) return { domains: {}, error: null };
|
|
5565
5609
|
try {
|
|
5566
|
-
const p = path.join(absRoot(root || process.cwd()), '.harness', 'quality-lenses.json');
|
|
5567
|
-
if (!exists(p)) return {};
|
|
5568
5610
|
const j = JSON.parse(read(p));
|
|
5569
|
-
return (j && typeof j === 'object' && j.domains && typeof j.domains === 'object') ? j.domains : {};
|
|
5611
|
+
return { domains: (j && typeof j === 'object' && j.domains && typeof j.domains === 'object') ? j.domains : {}, error: null };
|
|
5612
|
+
} catch (e) { return { domains: {}, error: `quality-lenses.json 파싱 실패(커스텀 렌즈 미적용): ${e.message}` }; }
|
|
5613
|
+
}
|
|
5614
|
+
function _loadProjectLenses(root) {
|
|
5615
|
+
try {
|
|
5616
|
+
return _loadProjectLensesEx(root).domains;
|
|
5570
5617
|
} catch { return {}; }
|
|
5571
5618
|
}
|
|
5572
5619
|
// 내장 + 프로젝트 커스텀 병합. 기존 도메인 → 질문 추가(dedup, 최대 16 — 내장 최대치(database 12) + 커스텀 여유; 낮으면 내장이 잘림); 신규 도메인 → 기본값으로 추가. _custom 플래그로 표시.
|
|
@@ -5576,7 +5623,9 @@ function _mergeLensCatalog(builtin, custom) {
|
|
|
5576
5623
|
for (const [k, c] of Object.entries(custom || {})) {
|
|
5577
5624
|
if (!c || typeof c !== 'object') continue;
|
|
5578
5625
|
const cq = Array.isArray(c.questions) ? c.questions.filter(q => typeof q === 'string' && q.trim()) : [];
|
|
5579
|
-
|
|
5626
|
+
// 1.36.42 (codex 5차 #9b): truthy 검사는 'constructor' 같은 프로토타입 키에서 상속 함수를 기존 도메인으로
|
|
5627
|
+
// 오인해 .questions.push 크래시 — 자기 소유 키만 병합 대상으로.
|
|
5628
|
+
if (Object.prototype.hasOwnProperty.call(out, k)) {
|
|
5580
5629
|
const seen = new Set(out[k].questions);
|
|
5581
5630
|
for (const q of cq) if (!seen.has(q)) { out[k].questions.push(q); seen.add(q); }
|
|
5582
5631
|
const before = out[k].questions.length;
|
|
@@ -5660,6 +5709,8 @@ function lensCmd(domain, opts = {}) {
|
|
|
5660
5709
|
if (domain != null) domain = String(domain).trim().toLowerCase();
|
|
5661
5710
|
// 1.19.3: 내장 + 프로젝트 커스텀(.harness/quality-lenses.json) 병합 catalog.
|
|
5662
5711
|
const root = opts.root || arg('--path', process.cwd());
|
|
5712
|
+
// 1.36.42 (#9): 손상 커스텀 렌즈 파일은 무언 무시 대신 경고 (advisory — lens 자체는 계속 동작)
|
|
5713
|
+
try { const _lx = _loadProjectLensesEx(root); if (_lx.error && !jsonMode) warn(_lx.error); } catch {}
|
|
5663
5714
|
// 1.36.30: 토글 준수 (⚙ 탭 / leerness toggle set lens off)
|
|
5664
5715
|
if (!_tgl.toggleOn(root, 'lens')) {
|
|
5665
5716
|
if (jsonMode) { log(JSON.stringify({ ok: true, skipped: true, reason: 'toggle_off', hint: 'leerness toggle set lens on' })); return; }
|
|
@@ -12375,7 +12426,11 @@ async function orchestrateCmd(root, goalParts) {
|
|
|
12375
12426
|
return process.exit(1);
|
|
12376
12427
|
}
|
|
12377
12428
|
|
|
12378
|
-
|
|
12429
|
+
// 1.36.42 (codex 5차 #5): NaN/0/-1 이 조용히 1(또는 NaN 루프)로 화하던 것 — 정수 1..256 검증.
|
|
12430
|
+
const _agRaw = arg('--agents', '4');
|
|
12431
|
+
const _agN = Number(_agRaw);
|
|
12432
|
+
if (!Number.isInteger(_agN) || _agN < 1 || _agN > 256) { failJson(has('--json'), 'invalid_agents', `--agents 는 1~256 정수여야 합니다 (받음: ${_agRaw})`); return; }
|
|
12433
|
+
const agentCount = _agN;
|
|
12379
12434
|
const model = arg('--model', process.env.LEERNESS_OLLAMA_MODEL || 'qwen2.5:7b-instruct');
|
|
12380
12435
|
const timeoutMs = parseInt(arg('--timeout', '300000'), 10);
|
|
12381
12436
|
const retryOnFail = parseInt(arg('--retry-on-fail', '0'), 10); // 1.9.22 후보 2 통합
|
|
@@ -12410,6 +12465,7 @@ async function orchestrateCmd(root, goalParts) {
|
|
|
12410
12465
|
if (failures.length) {
|
|
12411
12466
|
for (const f of failures.slice(0, 3)) log(` · agent ${f.agent}: ${f.error}`);
|
|
12412
12467
|
}
|
|
12468
|
+
if (!ok.length) process.exitCode = 1; // 1.36.42 (#5): 전원 실패인데 exit 0 이던 허위 성공 — 성공 0 = exit 1
|
|
12413
12469
|
|
|
12414
12470
|
if (ok.length) {
|
|
12415
12471
|
const totalPromptTokens = ok.reduce((a, b) => a + b.promptTokens, 0);
|
|
@@ -13974,28 +14030,33 @@ function registerPendingCmd(root, requestParts) {
|
|
|
13974
14030
|
const agent = arg('--agent', 'unknown');
|
|
13975
14031
|
const note = arg('--note', '');
|
|
13976
14032
|
|
|
13977
|
-
//
|
|
13978
|
-
|
|
13979
|
-
|
|
13980
|
-
|
|
13981
|
-
|
|
13982
|
-
|
|
13983
|
-
|
|
13984
|
-
|
|
13985
|
-
|
|
13986
|
-
|
|
13987
|
-
|
|
13988
|
-
|
|
13989
|
-
|
|
13990
|
-
|
|
13991
|
-
|
|
13992
|
-
|
|
13993
|
-
|
|
13994
|
-
|
|
14033
|
+
// 1.36.41 (codex 5차 #2, 1.36.31 락 클래스): ID 산출이 락 밖이라 병렬 등록 시 같은 T-ID 배정 → upsert 가
|
|
14034
|
+
// 서로를 덮어써 무성 유실(30중 25 잔존 실측; 다중 세션 등록이 이 명령의 명시 용도). ID 산출→행 작성→저장을 한 임계구역으로.
|
|
14035
|
+
const row = _withLock(progressPath(root), () => {
|
|
14036
|
+
const rows = readProgressRows(root);
|
|
14037
|
+
let maxN = 0;
|
|
14038
|
+
for (const r of rows) {
|
|
14039
|
+
const m = r.id && r.id.match(/^T-(\d+)$/);
|
|
14040
|
+
if (m) maxN = Math.max(maxN, parseInt(m[1], 10));
|
|
14041
|
+
}
|
|
14042
|
+
const id = `T-${String(maxN + 1).padStart(4, '0')}`;
|
|
14043
|
+
const rec = {
|
|
14044
|
+
id,
|
|
14045
|
+
status: 'in-progress',
|
|
14046
|
+
request: request.slice(0, 200),
|
|
14047
|
+
evidence: `(pending) by ${agent}${note ? ' — ' + note : ''}`,
|
|
14048
|
+
nextAction: '작업 진행 중',
|
|
14049
|
+
updated: today()
|
|
14050
|
+
};
|
|
14051
|
+
upsertProgress(root, rec);
|
|
14052
|
+
return rec;
|
|
14053
|
+
});
|
|
14054
|
+
const id = row.id;
|
|
14055
|
+
// 1.36.42 (codex 5차 #10): --json 인데 사람용 3줄이 앞에 섞여 파서가 깨짐 — JSON 모드는 단일 JSON 문서만.
|
|
14056
|
+
if (has('--json')) { log(JSON.stringify({ ok: true, id, ...row }, null, 2)); return; }
|
|
13995
14057
|
log(`✓ ${id} 등록됨 (in-progress) by ${agent}`);
|
|
13996
14058
|
log(` request: ${row.request}`);
|
|
13997
14059
|
log(` 💡 작업 완료 후: leerness task update ${id} --status done --evidence "..."`);
|
|
13998
|
-
if (has('--json')) log(JSON.stringify({ ok: true, id, ...row }, null, 2));
|
|
13999
14060
|
}
|
|
14000
14061
|
|
|
14001
14062
|
// 1.9.22 후보 4: llm-bench record + retro 통합
|
|
@@ -14038,10 +14099,20 @@ function mergeDesign(root) {
|
|
|
14038
14099
|
root = absRoot(root);
|
|
14039
14100
|
const canonical = path.join(root,'.harness/design-system.md');
|
|
14040
14101
|
const cands = ['designguide.md','design-guide.md','.harness/designguide.md','docs/designguide.md','docs/design-guide.md'];
|
|
14102
|
+
// 1.36.42 (codex 5차 #7): (a) --dry-run 인데 실기록, (b) 재실행마다 같은 내용 중복 append(비멱등) — 둘 다 교정.
|
|
14103
|
+
const dry = has('--dry-run');
|
|
14104
|
+
const already = exists(canonical) ? read(canonical) : '';
|
|
14041
14105
|
let merged = '';
|
|
14042
|
-
|
|
14106
|
+
const skipped = [];
|
|
14107
|
+
for (const f of cands) {
|
|
14108
|
+
const p = path.join(root,f);
|
|
14109
|
+
if (!exists(p)) continue;
|
|
14110
|
+
if (already.includes(`## Merged from ${f}`)) { skipped.push(f); continue; } // 멱등: 같은 소스 재병합 방지
|
|
14111
|
+
merged += `\n\n## Merged from ${f}\n\n` + read(p);
|
|
14112
|
+
}
|
|
14113
|
+
if (dry) { ok(`[dry-run] ${merged ? '병합 예정' : '병합 대상 없음'}${skipped.length ? ` · 이미 병합됨(skip): ${skipped.join(', ')}` : ''} — 실제 변경 0`); return; }
|
|
14043
14114
|
if (merged) append(canonical, merged);
|
|
14044
|
-
ok(merged ?
|
|
14115
|
+
ok(merged ? `design guides merged into .harness/design-system.md${skipped.length ? ` (이미 병합 skip: ${skipped.join(', ')})` : ''}` : (skipped.length ? `nothing to merge (이미 전부 병합됨: ${skipped.join(', ')})` : 'nothing to merge'));
|
|
14045
14116
|
}
|
|
14046
14117
|
|
|
14047
14118
|
// 1.9.2: self check를 update --check의 thin wrapper로 통합 (단일 출처).
|
|
@@ -15504,12 +15575,16 @@ function releaseBump(root) {
|
|
|
15504
15575
|
if (!exists(pkgFile)) return fail('package.json 없음');
|
|
15505
15576
|
let pkg; try { pkg = JSON.parse(read(pkgFile)); } catch (e) { return fail('package.json 파싱 실패: ' + e.message); }
|
|
15506
15577
|
const cur = String(pkg.version || '0.0.0');
|
|
15507
|
-
|
|
15508
|
-
|
|
15578
|
+
// 1.36.42 (codex 5차 #6): (a) 비 semver("banana")를 0.0.0 으로 오인해 0.0.1 로 덮어쓰던 것 → 거부.
|
|
15579
|
+
// (b) prerelease patch bump 는 npm 의미론(1.2.3-beta.2 → 1.2.3: pre 태그 제거=안정화)을 따름.
|
|
15580
|
+
const _vm = cur.match(/^(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?$/);
|
|
15581
|
+
if (!_vm) { failJson(has('--json'), 'invalid_version', `package.json version 이 semver 가 아님: "${cur}" — bump 거부(수동 교정 필요)`); return; }
|
|
15582
|
+
const [maj, min, pat] = [parseInt(_vm[1], 10), parseInt(_vm[2], 10), parseInt(_vm[3], 10)];
|
|
15583
|
+
const isPre = !!_vm[4];
|
|
15509
15584
|
let next;
|
|
15510
|
-
if (kind === 'major') next = `${maj + 1}.0.0`;
|
|
15511
|
-
else if (kind === 'minor') next = `${maj}.${min + 1}.0`;
|
|
15512
|
-
else next = `${maj}.${min}.${pat + 1}`;
|
|
15585
|
+
if (kind === 'major') next = isPre && min === 0 && pat === 0 ? `${maj}.0.0` : `${maj + 1}.0.0`;
|
|
15586
|
+
else if (kind === 'minor') next = isPre && pat === 0 ? `${maj}.${min}.0` : `${maj}.${min + 1}.0`;
|
|
15587
|
+
else next = isPre ? `${maj}.${min}.${pat}` : `${maj}.${min}.${pat + 1}`;
|
|
15513
15588
|
pkg.version = next;
|
|
15514
15589
|
writeUtf8(pkgFile, JSON.stringify(pkg, null, 2) + '\n');
|
|
15515
15590
|
const hv = path.join(root, '.harness/HARNESS_VERSION');
|
|
@@ -15520,6 +15595,9 @@ function releaseBump(root) {
|
|
|
15520
15595
|
function releaseNote(root, text) {
|
|
15521
15596
|
root = absRoot(root);
|
|
15522
15597
|
if (!text) return fail('note text required (e.g., release note "내용")');
|
|
15598
|
+
// 1.36.42 (codex 5차 #6b): 개행 포함 노트가 CHANGELOG 에 가짜 버전 헤더("## 9.9.9 …")를 만들 수 있었음 —
|
|
15599
|
+
// 멀티라인은 한 리스트 항목의 연속 들여쓰기로 정규화(헤더 문자로 시작하는 줄도 텍스트로 유지).
|
|
15600
|
+
text = String(text).replace(/\r\n?/g, '\n').split('\n').map((l, i) => i === 0 ? l : ' ' + l.replace(/^#+\s*/, '')).join('\n');
|
|
15523
15601
|
const pkgFile = path.join(root, 'package.json');
|
|
15524
15602
|
let version = 'unknown';
|
|
15525
15603
|
if (exists(pkgFile)) { try { version = JSON.parse(read(pkgFile)).version || 'unknown'; } catch {} }
|
|
@@ -19446,7 +19524,12 @@ function stateCmd(root, sub, ...args) {
|
|
|
19446
19524
|
function _mergeMcpJson(root) {
|
|
19447
19525
|
const f = path.join(absRoot(root), '.mcp.json');
|
|
19448
19526
|
let obj = {};
|
|
19449
|
-
|
|
19527
|
+
// 1.36.41 (codex 5차 #1, 1.36.28 손상-스토어 클래스): 손상 .mcp.json 을 {} 로 오인해 사용자의 다른 MCP 서버
|
|
19528
|
+
// 등록을 통째로 날렸다 — 파싱 실패/비객체 루트는 원본 보존 + 중단.
|
|
19529
|
+
if (exists(f)) {
|
|
19530
|
+
try { obj = JSON.parse(read(f)); } catch { throw Object.assign(new Error(`.mcp.json 이 손상돼(JSON 파싱 실패) 병합을 중단합니다 — 복구 후 재시도: ${f}`), { code: 'E_STORE_CORRUPT', file: f }); }
|
|
19531
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) throw Object.assign(new Error(`.mcp.json 루트가 객체가 아님 — 병합 중단: ${f}`), { code: 'E_STORE_CORRUPT', file: f });
|
|
19532
|
+
}
|
|
19450
19533
|
obj.mcpServers = obj.mcpServers || {};
|
|
19451
19534
|
const already = !!obj.mcpServers.leerness;
|
|
19452
19535
|
obj.mcpServers.leerness = { command: 'npx', args: ['leerness', 'mcp', 'serve'] };
|
|
@@ -19485,7 +19568,20 @@ function glossaryCmd(root, sub) {
|
|
|
19485
19568
|
// build (default)
|
|
19486
19569
|
const md = _renderGlossaryMd(entries, { lang: arg('--lang', 'both'), gaps });
|
|
19487
19570
|
const hd = path.join(root, '.harness'); mkdirp(hd);
|
|
19488
|
-
|
|
19571
|
+
// 1.36.41 (codex 5차 #3): 통파일 덮어쓰기가 마커 밖 수동 서문/부록을 삭제 — 기존 파일에 마커가 있으면
|
|
19572
|
+
// 생성 블록만 splice(마커가 존재하는 이유 그대로), 밖 내용은 보존.
|
|
19573
|
+
const _gf = path.join(hd, 'glossary.md');
|
|
19574
|
+
let _gOut = md;
|
|
19575
|
+
if (exists(_gf)) {
|
|
19576
|
+
const _prev = read(_gf);
|
|
19577
|
+
const _si = _prev.indexOf(GLOSSARY_START), _ei = _prev.indexOf(GLOSSARY_END);
|
|
19578
|
+
if (_si >= 0 && _ei > _si) {
|
|
19579
|
+
const _gsi = md.indexOf(GLOSSARY_START), _gei = md.indexOf(GLOSSARY_END);
|
|
19580
|
+
const _gen = (_gsi >= 0 && _gei > _gsi) ? md.slice(_gsi, _gei + GLOSSARY_END.length) : md;
|
|
19581
|
+
_gOut = _prev.slice(0, _si) + _gen + _prev.slice(_ei + GLOSSARY_END.length);
|
|
19582
|
+
}
|
|
19583
|
+
}
|
|
19584
|
+
writeUtf8(_gf, _gOut);
|
|
19489
19585
|
writeUtf8(path.join(hd, 'glossary.json'), JSON.stringify({ generated: today(), ...stats, entries, gaps }, null, 2) + '\n');
|
|
19490
19586
|
if (jsonMode) { log(JSON.stringify({ ok: true, action: 'glossary build', ...stats, file: '.harness/glossary.md' }, null, 2)); return; }
|
|
19491
19587
|
ok(`glossary 생성: .harness/glossary.md (${stats.defined}/${stats.total} 정의${stats.gaps ? ` · ${stats.gaps} 미정의 → AI 보완 프롬프트 포함` : ''})`);
|
|
@@ -19520,6 +19616,8 @@ function adapterCmd(root, tool, opts = {}) {
|
|
|
19520
19616
|
log(` → 실제 적용: leerness adapter ${tool}`);
|
|
19521
19617
|
return;
|
|
19522
19618
|
}
|
|
19619
|
+
// 1.36.41 (codex 5차 #1b): mcp 병합이 실패할 파일이면 지침 파일도 쓰기 전에 중단 — 부분 적용 방지(선검증).
|
|
19620
|
+
if (a.mcp) _assertStoreParsable(path.join(root, '.mcp.json'), '.mcp.json');
|
|
19523
19621
|
const results = [];
|
|
19524
19622
|
for (const k of a.keys) {
|
|
19525
19623
|
if (files[k] == null) continue;
|
|
@@ -21999,7 +22097,7 @@ async function main() {
|
|
|
21999
22097
|
if (cmd === 'adapter') {
|
|
22000
22098
|
const _aTool = args[1] && !args[1].startsWith('-') ? args[1] : 'list';
|
|
22001
22099
|
const _aRoot = (args[2] && !args[2].startsWith('-')) ? args[2] : arg('--path', process.cwd());
|
|
22002
|
-
return adapterCmd(_aRoot, _aTool);
|
|
22100
|
+
return _guardStore(has('--json'), () => adapterCmd(_aRoot, _aTool)); // 1.36.41 (#1): 손상 .mcp.json 은 구조화 에러 + exit 1
|
|
22003
22101
|
}
|
|
22004
22102
|
// 1.11.4 (UR-0007): glossary — 의존성 용어집(비개발자용 한 줄 설명). 외부 3-에이전트 평가 종합 설계(큐레이션 카탈로그, 무LLM).
|
|
22005
22103
|
if (cmd === 'glossary') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.42",
|
|
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",
|